文件

§使用 specs2 測試應用程式

為應用程式撰寫測試可能是一個複雜的過程。Play 為您提供預設的測試架構,並提供輔助程式和應用程式 Stub,讓測試應用程式變得盡可能簡單。

§概述

測試的位置在「test」資料夾中。在測試資料夾中建立了兩個範例測試檔案,可用作範本。

您可以在 Play 主控台中執行測試。

Play 中的測試基於 sbt,完整的說明可以在 測試 sbt 章節中找到。

§使用 specs2

若要使用 Play 的 specs2 支援,請將 Play specs2 相依項新增到您的建置中,作為測試範圍的相依項

libraryDependencies += specs2 % Test

specs2 中,測試組織成規格,其中包含範例,這些範例會透過各種不同的程式碼路徑執行受測系統。

規格擴充 Specification 特質,並使用 should/in 格式

import org.specs2.mutable._

class HelloWorldSpec extends Specification {
  "The 'Hello world' string" should {
    "contain 11 characters" in {
      "Hello world" must have size 11
    }
    "start with 'Hello'" in {
      "Hello world" must startWith("Hello")
    }
    "end with 'world'" in {
      "Hello world" must endWith("world")
    }
  }
}

規格可以在 IntelliJ IDEA(使用 Scala 外掛程式)或 Eclipse(使用 Scala IDE)中執行。請參閱 IDE 頁面 以取得更多詳細資訊。

注意:由於 展示編譯器 中的錯誤,測試必須定義為特定格式才能在 Eclipse 中使用

以下是 Eclipse 中有效的規格

package models

import org.junit.runner.RunWith
import org.specs2.mutable.Specification
import org.specs2.runner.JUnitRunner

@RunWith(classOf[JUnitRunner])
class UserSpec extends Specification {
  "User" should {
    "have a name" in {
      val user = User(id = "user-id", name = "Player", email = "[email protected]")
      user.name must beEqualTo("Player")
    }
  }
}

§比對器

使用範例時,您必須傳回範例結果。通常,您會看到包含 must 的陳述式

"Hello world" must endWith("world")

must 關鍵字之後的表達式稱為 比對器。比對器傳回範例結果,通常為 Success 或 Failure。如果範例未傳回結果,則無法編譯。

最實用的比對器是 比對結果。這些用於檢查相等性、判斷 Option 和 Either 的結果,甚至檢查是否引發例外狀況。

還有 選用比對器,允許在測試中進行 XML 和 JSON 比對。

§Mockito

模擬用於隔離單元測試與外部依賴項。例如,如果您的類別依賴於外部 DataService 類別,您可以在不建立 DataService 物件的情況下將適當的資料提供給類別。

要使用 Mockito(一個廣受歡迎的模擬函式庫),請新增以下匯入

import org.mockito.Mockito._

您可以這樣模擬類別的參考

trait DataService {
  def findData: Data
}

case class Data(retrievalDate: java.util.Date)
import java.util._

import org.mockito.Mockito._
import org.specs2.mutable._

class ExampleMockitoSpec extends Specification {
  "MyService#isDailyData" should {
    "return true if the data is from today" in {
      val mockDataService = mock(classOf[DataService])
      when(mockDataService.findData).thenReturn(Data(retrievalDate = new java.util.Date()))

      val myService = new MyService() {
        override def dataService = mockDataService
      }

      val actual = myService.isDailyData
      actual must equalTo(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 Specification {
  "UserService#isAdmin" should {
    "be true when the role is admin" in {
      val userRepository = mock(classOf[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 must beTrue
    }
  }
}

§單元測試控制器

由於控制器只是常規類別,因此你可以輕鬆使用 Play 輔助程式對它們進行單元測試。如果控制器依賴於其他類別,則使用 依賴注入 將使你能夠模擬這些依賴項。例如,給定以下控制器

class ExampleController @Inject() (cc: ControllerComponents) extends AbstractController(cc) {
  def index = Action {
    Ok("ok")
  }
}

你可以像這樣測試它

import javax.inject.Inject

import scala.concurrent.Future

import play.api.data.FormBinding.Implicits._
import play.api.i18n.Messages
import play.api.mvc._
import play.api.test._

class ExampleControllerSpec extends PlaySpecification with Results {
  "Example Page#index" should {
    "be valid" in {
      val controller             = new ExampleController(Helpers.stubControllerComponents())
      val result: Future[Result] = controller.index.apply(FakeRequest())
      val bodyText: String       = contentAsString(result)
      (bodyText must be).equalTo("ok")
    }
  }
}

§StubControllerComponents

StubControllerComponentsFactory 會建立一個 stub ControllerComponents,可用於對控制器進行單元測試

val controller = new MyController(
  Helpers.stubControllerComponents(bodyParser = stubParser)
)

§StubBodyParser

StubBodyParserFactory 會建立一個 stub BodyParser,可用於對內容進行單元測試

val stubParser = Helpers.stubBodyParser(AnyContent("hello"))

§單元測試表單

表單也是常規類別,可以使用 Play 的測試輔助程式對其進行單元測試。使用 FakeRequest,你可以呼叫 form.bindFromRequest 並針對任何自訂約束測試錯誤。

若要對表單處理進行單元測試並呈現驗證錯誤,您會需要在隱含範圍中使用 MessagesApi 執行個體。 MessagesApi 的預設實作是 DefaultMessagesApi

你可以像這樣測試它

object FormData {
  import play.api.data._
  import play.api.data.Forms._
  import play.api.i18n._
  import play.api.libs.json._

  val form = Form(
    mapping(
      "name" -> text,
      "age"  -> number(min = 0)
    )(UserData.apply)(UserData.unapply)
  )

  case class UserData(name: String, age: Int)
  object UserData {
    def unapply(u: UserData): Option[(String, Int)] = Some(u.name, u.age)
  }
}

class ExampleFormSpec extends PlaySpecification with Results {
  import play.api.data._
  import play.api.i18n._
  import play.api.libs.json._
  import FormData._

  "Form" should {
    "be valid" in {
      val messagesApi = new DefaultMessagesApi(
        Map(
          "en" ->
            Map("error.min" -> "minimum!")
        )
      )
      implicit val request: FakeRequest[AnyContentAsFormUrlEncoded] = {
        FakeRequest("POST", "/")
          .withFormUrlEncodedBody("name" -> "Play", "age" -> "-1")
      }
      implicit val messages: Messages = messagesApi.preferred(request)

      def errorFunc(badForm: Form[UserData]) = {
        BadRequest(badForm.errorsAsJson)
      }

      def successFunc(userData: UserData) = {
        Redirect("/").flashing("success" -> "success form!")
      }

      val result = Future.successful(form.bindFromRequest().fold(errorFunc, successFunc))
      Json.parse(contentAsString(result)) must beEqualTo(Json.obj("age" -> Json.arr("minimum!")))
    }
  }
}

在呈現使用表單輔助工具的範本時,您可以以相同的方式傳入訊息,或使用 Helpers.stubMessages()

class ExampleTemplateSpec extends PlaySpecification {
  import play.api.data._
  import FormData._

  "Example Template with Form" should {
    "be valid" in {
      val form: Form[UserData]        = FormData.form
      implicit val messages: Messages = Helpers.stubMessages()
      contentAsString(views.html.formTemplate(form)) must contain("ok")
    }
  }
}

或者,如果您使用的是使用 CSRF.formField 並需要隱含要求的表單,您可以在範本中使用 MessagesRequest 並使用 Helpers.stubMessagesRequest()

class ExampleTemplateWithCSRFSpec extends PlaySpecification {
  import play.api.data._
  import FormData._

  "Example Template with Form" should {
    "be valid" in {
      val form: Form[UserData]                                 = FormData.form
      implicit val messageRequestHeader: MessagesRequestHeader = Helpers.stubMessagesRequest()
      contentAsString(views.html.formTemplateWithCSRF(form)) must contain("ok")
    }
  }
}

§單元測試 EssentialAction

測試 ActionFilter 可能需要測試 EssentialAction有關 EssentialAction 的更多資訊

為此,測試 Helpers.call() 可以像這樣使用

class ExampleEssentialActionSpec extends PlaySpecification {
  "An essential action" should {
    "can parse a JSON body" in new WithApplication() with Injecting {
      override def running() = {
        val Action = inject[DefaultActionBuilder]
        val parse  = inject[PlayBodyParsers]

        val action: EssentialAction = Action(parse.json) { request =>
          val value = (request.body \ "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"
      }
    }
  }
}

§單元測試訊息

對於單元測試目的,DefaultMessagesApi 可以不帶引數地實例化,並會採用原始映射,因此您可以針對自訂 MessagesApi 測試表單和驗證失敗

class ExampleMessagesSpec extends PlaySpecification with ControllerHelpers {
  import play.api.data.Form
  import play.api.data.FormBinding.Implicits._
  import play.api.data.Forms._
  import play.api.i18n._
  import play.api.libs.json.Json

  case class UserData(name: String, age: Int)
  object UserData {
    def unapply(u: UserData): Option[(String, Int)] = Some(u.name, u.age)
  }

  "Messages test" should {
    "test messages validation in forms" in {
      // Define a custom message against the number validation constraint
      val messagesApi = new DefaultMessagesApi(
        Map("en" -> Map("error.min" -> "CUSTOM MESSAGE"))
      )

      // Called when form validation fails
      def errorFunc(badForm: Form[UserData])(implicit request: RequestHeader) = {
        implicit val messages: Messages = messagesApi.preferred(request)
        BadRequest(badForm.errorsAsJson)
      }

      // Called when form validation succeeds
      def successFunc(userData: UserData) = Redirect("/")

      // Define an age with 0 as the minimum
      val form = Form(
        mapping("name" -> text, "age" -> number(min = 0))(UserData.apply)(UserData.unapply)
      )

      // Submit a request with age = -1
      implicit val request: FakeRequest[AnyContentAsFormUrlEncoded] = {
        play.api.test
          .FakeRequest("POST", "/")
          .withFormUrlEncodedBody("name" -> "Play", "age" -> "-1")
      }

      // Verify that the "error.min" is the custom message
      val result = Future.successful(form.bindFromRequest().fold(errorFunc, successFunc))
      Json.parse(contentAsString(result)) must beEqualTo(Json.obj("age" -> Json.arr("CUSTOM MESSAGE")))
    }
  }
}

您還可以在測試中使用 Helpers.stubMessagesApi() 來提供預先製作的空 MessagesApi

下一頁:使用 specs2 撰寫功能測試


在這個文件中發現錯誤了嗎?此頁面的原始碼可以在 這裡 找到。在閱讀完 文件指南 後,請隨時做出拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。