ドキュメント

§specs2を使用したアプリケーションのテスト

アプリケーションのテストを作成するには、複雑なプロセスが必要になる場合があります。Playには、デフォルトのテストフレームワークが用意されており、アプリケーションのテストをできるだけ簡単にするためのヘルパーとアプリケーションスタブが用意されています。

§概要

テストの場所は「test」フォルダーです。testフォルダーには、テンプレートとして使用できる2つのサンプルテストファイルが作成されています。

Playコンソールからテストを実行できます。

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で動作するようにテストを特定の形式で定義する必要があります。

以下は、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()を使用することもできます。

次へ: specs2を使った機能テストの作成


このドキュメントにエラーを見つけましたか?このページのソースコードはこちらにあります。 ドキュメントのガイドラインを読んだ後、プルリクエストを自由に投稿してください。質問や共有したいアドバイスはありますか?コミュニティフォーラムに参加して、コミュニティとの会話を始めましょう。