§使用 ScalaTest 测试您的应用程序
为您的应用程序编写测试可能是一个复杂的过程。Play 提供了助手和应用程序存根,而 ScalaTest 提供了一个集成库,ScalaTest + Play,使测试您的应用程序尽可能容易。
§概述
测试的位置在“test”文件夹中。
您可以从 Play 控制台运行测试。
- 要运行所有测试,请运行
test
。 - 要仅运行一个测试类,请运行
test-only
,后跟类的名称,例如test-only my.namespace.MySpec
。 - 要仅运行失败的测试,请运行
test-quick
。 - 要持续运行测试,请运行以波浪号开头的命令,例如
~test-quick
。 - 要访问控制台中的测试助手,例如
FakeRequest
,请运行test:console
。
Play 中的测试基于 SBT,完整的描述可以在 测试 SBT 章节中找到。
§使用 ScalaTest + Play
要使用 ScalaTest + Play,您需要将其添加到您的构建中,方法是像这样更改 build.sbt
libraryDependencies ++= Seq(
"org.scalatestplus.play" %% "scalatestplus-play" % "x.x.x" % Test
)
其中x.x.x
是 scalatestplus-play
工件的特定版本,例如 5.1.0
。请查看 此处提供的版本。
您无需在构建中显式添加 ScalaTest 或 ScalaTest plus mockito。ScalaTest 的正确版本将作为 ScalaTest + Play 的传递依赖项自动引入。但是,您需要选择与 Play 版本匹配的 ScalaTest + Play 版本。您可以通过查看 ScalaTest + Play 的 版本兼容性 矩阵来实现。
在 ScalaTest + Play 中,您可以通过扩展 PlaySpec
特性来定义测试类。以下是一个示例
import org.scalatestplus.play._
import scala.collection.mutable
class StackSpec extends PlaySpec {
"A Stack" must {
"pop values in last-in-first-out order" in {
val stack = new mutable.Stack[Int]
stack.push(1)
stack.push(2)
stack.pop() mustBe 2
stack.pop() mustBe 1
}
"throw NoSuchElementException if an empty stack is popped" in {
val emptyStack = new mutable.Stack[Int]
a[NoSuchElementException] must be thrownBy {
emptyStack.pop()
}
}
}
}
您可以选择 定义自己的基类,而不是使用 PlaySpec
。
您可以使用 Play 本身运行测试,或者在 IntelliJ IDEA(使用 Scala 插件)或 Eclipse(使用 Scala IDE 和 ScalaTest Eclipse 插件)中运行。请查看 IDE 页面 获取更多详细信息。
§匹配器
PlaySpec
混合了 ScalaTest 的 MustMatchers
,因此您可以使用 ScalaTest 的匹配器 DSL 编写断言
import play.api.test.Helpers._
"Hello world" must endWith ("world")
有关更多信息,请查看 MustMatchers
的文档。
§Mockito
您可以使用模拟对象将单元测试与外部依赖项隔离。例如,如果您的类依赖于外部 DataService
类,您可以向您的类提供适当的数据,而无需实例化 DataService
对象。
ScalaTest 通过其 MockitoSugar
特性提供了与 Mockito 的集成。
要使用 Mockito,请将 MockitoSugar
混合到您的测试类中,然后使用 Mockito 库模拟依赖项
case class Data(retrievalDate: java.util.Date)
trait DataService {
def findData: Data
}
import org.scalatestplus.mockito.MockitoSugar
import org.scalatestplus.play._
import org.mockito.Mockito._
class ExampleMockitoSpec extends PlaySpec with MockitoSugar {
"MyService#isDailyData" should {
"return true if the data is from today" in {
val mockDataService = mock[DataService]
when(mockDataService.findData).thenReturn(Data(new java.util.Date()))
val myService = new MyService() {
override def dataService = mockDataService
}
val actual = myService.isDailyData
actual mustBe true
}
}
}
模拟对于测试类的公共方法特别有用。模拟对象和私有方法是可能的,但要困难得多。
§单元测试模型
Play 不要求模型使用特定的数据库数据访问层。但是,如果应用程序使用 Anorm 或 Slick,那么模型通常会在内部引用数据库访问。
import anorm._
import anorm.SqlParser._
case class User(id: String, name: String, email: String) {
def roles = DB.withConnection { implicit connection =>
...
}
}
对于单元测试,这种方法可能会使模拟 roles
方法变得很棘手。
一种常见的方法是将模型与数据库隔离,并尽可能多地隔离逻辑,并将数据库访问抽象到存储库层。
case class Role(name: String)
case class User(id: String, name: String, email: String)
trait UserRepository {
def roles(user: User): Set[Role]
}
class AnormUserRepository extends UserRepository {
import anorm._
import anorm.SqlParser._
def roles(user:User) : Set[Role] = {
...
}
}
然后通过服务访问它们
class UserService(userRepository: UserRepository) {
def isAdmin(user: User): Boolean = {
userRepository.roles(user).contains(Role("ADMIN"))
}
}
这样,isAdmin
方法可以通过模拟 UserRepository
引用并将其传递给服务来进行测试。
class UserServiceSpec extends PlaySpec with MockitoSugar {
"UserService#isAdmin" should {
"be true when the role is admin" in {
val userRepository = mock[UserRepository]
when(userRepository.roles(any[User])).thenReturn(Set(Role("ADMIN")))
val userService = new UserService(userRepository)
val actual = userService.isAdmin(User("11", "Steve", "[email protected]"))
actual mustBe true
}
}
}
§单元测试控制器
由于您的控制器只是普通的类,您可以使用 Play 助手轻松地对其进行单元测试。如果您的控制器依赖于其他类,使用 依赖注入 将使您能够模拟这些依赖项。例如,给定以下控制器
class ExampleController(val controllerComponents: ControllerComponents) extends BaseController {
def index() = Action {
Ok("ok")
}
}
您可以像这样测试它
import scala.concurrent.Future
import org.scalatestplus.play._
import play.api.mvc._
import play.api.test._
import play.api.test.Helpers._
class ExampleControllerSpec extends PlaySpec with Results {
"Example Page#index" should {
"should be valid" in {
val controller = new ExampleController(Helpers.stubControllerComponents())
val result: Future[Result] = controller.index().apply(FakeRequest())
val bodyText: String = contentAsString(result)
bodyText mustBe "ok"
}
}
}
§单元测试 EssentialAction
测试 Action
或 Filter
可能需要测试 EssentialAction
(有关 EssentialAction 的更多信息)
为此,可以使用测试 Helpers.call
,如下所示
class ExampleEssentialActionSpec extends PlaySpec with GuiceOneAppPerSuite {
implicit lazy val materializer: Materializer = app.materializer
implicit lazy val Action: DefaultActionBuilder = app.injector.instanceOf(classOf[DefaultActionBuilder])
"An essential action" should {
"can parse a JSON body" in {
val action: EssentialAction = Action { request =>
val value = (request.body.asJson.get \ "field").as[String]
Ok(value)
}
val request = FakeRequest(POST, "/").withJsonBody(Json.parse("""{ "field": "value" }"""))
val result = call(action, request)
status(result) mustEqual OK
contentAsString(result) mustEqual "value"
}
}
}
在此文档中发现错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区开始对话。