§specs2を使用したアプリケーションのテスト
アプリケーションのテストを作成するには、複雑なプロセスが必要になる場合があります。Playには、デフォルトのテストフレームワークが用意されており、アプリケーションのテストをできるだけ簡単にするためのヘルパーとアプリケーションスタブが用意されています。
§概要
テストの場所は「test」フォルダーです。testフォルダーには、テンプレートとして使用できる2つのサンプルテストファイルが作成されています。
Playコンソールからテストを実行できます。
- すべてのテストを実行するには、
test
を実行します。 - 1つのテストクラスのみを実行するには、
test-only
にクラス名(例:test-only my.namespace.MySpec
)を続けます。 - 失敗したテストのみを実行するには、
test-quick
を実行します。 - テストを継続的に実行するには、先頭にチルダを付けたコマンド(例:
~test-quick
)を実行します。 - コンソールで
FakeRequest
などのテストヘルパーにアクセスするには、Test/console
を実行します。
Playでのテストはsbtに基づいており、詳細な説明はtesting 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
キーワードの後に続く式は、matchers
と呼ばれています。マッチャーは、通常、成功または失敗の例の結果を返します。結果を返さない場合、例はコンパイルされません。
最も便利なマッチャーは、一致結果です。これらは、等価性をチェックしたり、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
は、コントローラーの単体テストに使用できるスタブControllerComponents
を作成します。
val controller = new MyController(
Helpers.stubControllerComponents(bodyParser = stubParser)
)
§StubBodyParser
StubBodyParserFactory
は、コンテンツの単体テストに使用できるスタブ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")))
}
}
}
テストでは、事前に用意された空のMessagesApi
を提供するために、Helpers.stubMessagesApi()
を使用することもできます。
このドキュメントにエラーを見つけましたか?このページのソースコードはこちらにあります。 ドキュメントのガイドラインを読んだ後、プルリクエストを自由に投稿してください。質問や共有したいアドバイスはありますか?コミュニティフォーラムに参加して、コミュニティとの会話を始めましょう。