§使用 specs2 測試應用程式
為應用程式撰寫測試可能是一個複雜的過程。Play 為您提供預設的測試架構,並提供輔助程式和應用程式 Stub,讓測試應用程式變得盡可能簡單。
§概述
測試的位置在「test」資料夾中。在測試資料夾中建立了兩個範例測試檔案,可用作範本。
您可以在 Play 主控台中執行測試。
- 若要執行所有測試,請執行
test
。 - 若要只執行一個測試類別,請執行
test-only
,後面接類別名稱,例如test-only my.namespace.MySpec
。 - 若要只執行失敗的測試,請執行
test-quick
。 - 若要持續執行測試,請在命令前面加上波浪符號,例如
~test-quick
。 - 若要在主控台中存取測試輔助程式,例如
FakeRequest
,請執行Test/console
。
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 中使用
- 套件必須與目錄路徑完全相同。
- 規格必須加上
@RunWith(classOf[JUnitRunner])
註解。
以下是 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
測試 Action
或 Filter
可能需要測試 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 撰寫功能測試
在這個文件中發現錯誤了嗎?此頁面的原始碼可以在 這裡 找到。在閱讀完 文件指南 後,請隨時做出拉取請求。有問題或建議要分享嗎?請前往 我們的社群論壇 與社群展開對話。