文档

§使用 specs2 编写功能测试

Play 提供了许多类和便利方法来帮助进行功能测试。大多数这些方法可以在 play.api.test 包或 Helpers 对象中找到。

您可以通过导入以下内容来添加这些方法和类

import play.api.test._
import play.api.test.Helpers._

§创建用于测试的 Application 实例

Play 通常需要一个正在运行的 Application 作为上下文。如果您使用的是默认的 Guice 依赖注入,则可以使用 GuiceApplicationBuilder 类,该类可以使用不同的配置、路由甚至额外的模块进行配置。

val application: Application = GuiceApplicationBuilder().build()

§WithApplication

要将应用程序传递给示例,请使用 WithApplication。可以传递显式的 Application,但为了方便起见,提供了默认应用程序(从默认的 GuiceApplicationBuilder 创建)。

因为 WithApplication 是一个内置的 Around 块,您可以覆盖它来提供自己的数据填充

abstract class WithDbData extends WithApplication {
  override def wrap[T: AsResult](t: => T): Result = super.wrap {
    setupData()
    t
  }

  def setupData(): Unit = {
    // setup data
  }
}

"Computer model" should {
  "be retrieved by id" in new WithDbData {
    override def running() = {
      // your test code
    }
  }
  "be retrieved by email" in new WithDbData {
    override def running() = {
      // your test code
    }
  }
}

§WithServer

有时您想从测试中测试真实的 HTTP 堆栈,在这种情况下,您可以使用 WithServer 启动测试服务器

"test server logic" in new WithServer(app = applicationWithBrowser, port = testPort) {
  override def running() = {
    // The test payment gateway requires a callback to this server before it returns a result...
    val callbackURL = s"http://$myPublicAddress/callback"

    val ws = app.injector.instanceOf[WSClient]

    // await is from play.api.test.FutureAwaits
    val response =
      await(ws.url(testPaymentGatewayURL).withQueryStringParameters("callbackURL" -> callbackURL).get())

    response.status must equalTo(OK)
  }
}

port 值包含服务器正在运行的端口号。默认情况下会分配随机端口,但是您可以通过将端口传递给 WithServer 或设置系统属性 testserver.port 来更改它。这对于与持续集成服务器集成很有用,以便可以为每个构建动态保留端口。您还可以使用系统属性 testserver.address 配置测试服务器绑定的地址。如果未设置,它使用 Play 服务器默认值 "0.0.0.0"

应用程序也可以传递给测试服务器,这对于设置自定义路由和测试 WS 调用很有用。

val appWithRoutes = GuiceApplicationBuilder()
  .appRoutes { app =>
    val Action = app.injector.instanceOf[DefaultActionBuilder]
    ({
      case ("GET", "/") =>
        Action {
          Ok("ok")
        }
    })
  }
  .build()

"test WSClient logic" in new WithServer(app = appWithRoutes, port = 3333) {
  override def running() = {
    val ws = app.injector.instanceOf[WSClient]
    await(ws.url("http://localhost:3333").get()).status must equalTo(OK)
  }
}

§使用浏览器

如果你想使用浏览器测试你的应用程序,你可以使用 Selenium WebDriver。Play 会为你启动 WebDriver,并使用 FluentLenium 提供的便捷 API 将其包装起来,使用 WithBrowser。就像 WithServer 一样,你可以更改端口、Application,你也可以选择要使用的 Web 浏览器。

def applicationWithBrowser = {
  new GuiceApplicationBuilder()
    .appRoutes { app =>
      val Action = app.injector.instanceOf[DefaultActionBuilder]
      ({
        case ("GET", "/") =>
          Action {
            Ok("""
                 |<html>
                 |<body>
                 |  <div id="title">Hello Guest</div>
                 |  <a href="/login">click me</a>
                 |</body>
                 |</html>
            """.stripMargin).as("text/html")
          }
        case ("GET", "/login") =>
          Action {
            Ok("""
                 |<html>
                 |<body>
                 |  <div id="title">Hello Coco</div>
                 |</body>
                 |</html>
            """.stripMargin).as("text/html")
          }
      })
    }
    .build()
}

"run in a browser" in new WithBrowser(webDriver = WebDriverFactory(HTMLUNIT), app = applicationWithBrowser) {
  override def running() = {
    browser.goTo("/")

    // Check the page
    browser.el("#title").text() must equalTo("Hello Guest")

    browser.el("a").click()

    browser.url must equalTo("login")
    browser.el("#title").text() must equalTo("Hello Coco")
  }
}

§注入

许多功能测试直接通过隐式 app 使用注入器。

"test" in new WithApplication() {
  override def running() = {
    val executionContext = app.injector.instanceOf[ExecutionContext]
    executionContext must beAnInstanceOf[ExecutionContext]
  }
}

使用 Injecting 特性,你可以省略它。

"test" in new WithApplication() with play.api.test.Injecting {
  override def running() = {
    val executionContext = inject[ExecutionContext]
    executionContext must beAnInstanceOf[ExecutionContext]
  }
}

§PlaySpecification

PlaySpecificationSpecification 的扩展,它排除了默认 specs2 规范中提供的一些与 Play 助手方法冲突的混合。

class ExamplePlaySpecificationSpec extends PlaySpecification {
  "The specification" should {
    "have access to HeaderNames" in {
      USER_AGENT must be_===("User-Agent")
    }

    "have access to Status" in {
      OK must be_===(200)
    }
  }
}

§测试视图模板

由于模板是一个标准的 Scala 函数,你可以从你的测试中执行它,并检查结果。

"render index template" in new WithApplication {
  override def running() = {
    val html = views.html.index("Coco")

    contentAsString(html) must contain("Hello Coco")
  }
}

§测试控制器

你可以通过提供 FakeRequest 来调用任何 Action 代码。

"respond to the index Action" in new WithApplication {
  override def running() = {
    val controller = app.injector.instanceOf[scalaguide.tests.controllers.HomeController]
    val result     = controller.index()(FakeRequest())

    status(result) must equalTo(OK)
    contentType(result) must beSome("text/plain")
    contentAsString(result) must contain("Hello Bob")
  }
}

从技术上讲,你不需要 WithApplication,因为你可以直接实例化控制器 - 然而,直接控制器实例化更像是对控制器的单元测试,而不是功能测试。

§测试路由器

与其自己调用 Action,不如让 Router 来做。

"respond to the index Action" in new WithApplication(applicationWithRouter) {
  override def running() = {
val Some(result) = route(app, FakeRequest(GET, "/Bob"))

    status(result) must equalTo(OK)
    contentType(result) must beSome("text/html")
    charset(result) must beSome("utf-8")
    contentAsString(result) must contain("Hello Bob")
  }
}

§测试模型

如果你使用的是 SQL 数据库,你可以使用 inMemoryDatabase 将数据库连接替换为内存中的 H2 数据库实例。

def appWithMemoryDatabase = new GuiceApplicationBuilder().configure(inMemoryDatabase("test")).build()
"run an application" in new WithApplication(appWithMemoryDatabase) {
  override def running() = {
    val Some(macintosh) = Computer.findById(21)

    macintosh.name must equalTo("Macintosh")
    macintosh.introduced must beSome[String].which(_ must beEqualTo("1984-01-24"))
  }
}

§测试消息 API

对于涉及配置的功能测试,最佳选择是使用 WithApplication 并拉取注入的 MessagesApi

"messages" should {
  import play.api.i18n._

  implicit val lang = Lang("en-US")

  "provide default messages with the Java API" in new WithApplication() with Injecting {
    override def running() = {
      val javaMessagesApi = inject[play.i18n.MessagesApi]
      val msg             = javaMessagesApi.get(new play.i18n.Lang(lang), "constraint.email")
      msg must ===("Email")
    }
  }

  "provide default messages with the Scala API" in new WithApplication() with Injecting {
    override def running() = {
      val messagesApi = inject[MessagesApi]
      val msg         = messagesApi("constraint.email")
      msg must ===("Email")
    }
  }
}

如果你需要自定义配置,最好将配置值添加到 GuiceApplicationBuilder 中,而不是直接使用 DefaultMessagesApiProvider

下一步:使用 Guice 进行测试


发现此文档中的错误?此页面的源代码可以在 此处 找到。在阅读 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?前往 我们的社区论坛 与社区进行交流。