文件

§使用 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("https://127.0.0.1:3333").get()).status must equalTo(OK)
  }
}

§WithBrowser

如果你想要使用瀏覽器來測試你的應用程式,你可以使用 Selenium WebDriver。Play 會為你啟動 WebDriver,並使用 FluentLenium 提供的便利 API 將其包裝在 WithBrowser 中。就像 WithServer 一樣,你可以變更埠、Application,你也可以選擇要使用的網路瀏覽器

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")
  }
}

§Injecting

有許多功能測試會透過隱含的 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 輔助函式方法衝突。它也會混入 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,因為你可以直接實例化控制器,但直接實例化控制器比較像是控制器的單元測試,而不是功能測試。

§測試路由器

你可以讓 Router 來呼叫 Action,而不是自己呼叫

"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 進行測試


在這個文件中發現錯誤嗎?此頁面的原始程式碼可以在 這裡 找到。在閱讀 文件指南 後,歡迎貢獻一個 pull request。有問題或建議要分享嗎?前往 我們的社群論壇,與社群開始對話。