Documentation

You are viewing the documentation for the 2.6.x development release. The latest stable release series is 2.4.x.

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

アプリケーションのテストを作成するのは複雑な作業となりがちです。Play はヘルパーやスタブを提供し、ScalaTest は統合ライブラリ「 ScalaTest + Play 」を提供しており、テストの作成をできる限り容易にしています。

§概要

テストのソースファイルは “test” フォルダに配置します。

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

Play のテストは SBT に基づいており、 testing SBT に詳細が記載されています。

§ScalaTest + Play を使う

ScalaTest + Play を使用するには、 build.sbt を以下のように変更し、ビルドに加えます。

libraryDependencies ++= Seq(
  "org.scalatest" %% "scalatest" % "2.2.1" % "test",
  "org.scalatestplus" %% "play" % "1.4.0-M3" % "test",
)

ScalaTest を明示的にビルドに追加する必要はありません。適切なバージョンの ScalaTest が ScalaTest + Play の推移従属性にしたがって自動的に取得されます。しかし、特定の Play のバージョンに合った ScalaTest + Play のバージョンを選択する必要があるかもしれません。その方法は Versions, Versions, Versions ページに記載されています。

ScalaTest + Play では、 PlaySpec トレイトを拡張することでテストクラスを定義します。以下がその例です。

import collection.mutable.Stack
import org.scalatestplus.play._

class StackSpec extends PlaySpec {

  "A Stack" must {
    "pop values in last-in-first-out order" in {
      val stack = new Stack[Int]
      stack.push(1)
      stack.push(2)
      stack.pop() mustBe 2
      stack.pop() mustBe 1
    }
    "throw NoSuchElementException if an empty stack is popped" in {
      val emptyStack = new Stack[Int]
      a [NoSuchElementException] must be thrownBy {
        emptyStack.pop()
      }
    }
  }
}

あるいは、PlaySpec のかわりに 自前のベースクラスを定義する こともできます。

テストは Play 自体、あるいは (Scala plugin を使用した) IntelliJ IDEA、(Scala IDEScalaTest Eclipse plugin) を使用した Eclipse によって実行できます。詳しくは IDE のページ を参照してください。

§Matchers

PlaySpec は ScalaTest の MustMatchers をミックスインしているので、 ScalaTest の matchers DSL を使用してアサーションを書くことができます。

import play.api.test.Helpers._

"Hello world" must endWith ("world")

詳しくは、 MustMatchers のドキュメントを参照してください。

§Mockito

ユニットテストを外部の依存性から隔離するためにモックを利用できます。例えば、テスト対象クラスが外部の DataService クラスに依存している場合、 DataService オブジェクトをインスタンス化することなく適当なデータをテスト対象クラスに与えることができます。

ScalaTest は MockitoSugar トレイトを介して Mockito との統合を提供しています。

Mockito を使用するには、 MockitoSugar をテストクラスにミックスインし、 依存性をモックするために Mockito ライブラリを使用します。

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

trait DataService {
  def findData: Data
}
import org.scalatest._
import org.scalatest.mock.MockitoSugar
import org.scalatestplus.play._

import org.mockito.Mockito._

class ExampleMockitoSpec extends PlaySpec with MockitoSugar {

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

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

      val actual = myService.isDailyData
      actual mustBe true
    }
  }
}

モックは特に、public なメソッドに対してのテストに便利です。object や private メソッドのモックも可能ではありますが、非常に困難です。

§モデルのユニットテスト

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

こうすることで、モック化した UserRepository の参照をサービスに渡して isAdmin メソッドをテストすることができます

class UserServiceSpec extends PlaySpec with MockitoSugar {

  "UserService#isAdmin" should {
    "be true when the role is admin" in {
      val userRepository = mock[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 mustBe true
    }
  }
}

§コントローラのユニットテスト

コントローラを object として定義すると、ユニットテストするのが難しくなります。Play では 依存性注入 で緩和できます。あるいは、コントローラに 明示的に型付けられた自己参照 のトレイトを使用するという方法によっても、object として定義されたコントローラのユニットテストを幾分楽にすることができます。

trait ExampleController {
  this: Controller =>

  def index() = Action {
    Ok("ok")
  }
}

object ExampleController extends Controller with ExampleController

そして、トレイトのテストを追加します。

import scala.concurrent.Future

import org.scalatest._
import org.scalatestplus.play._

import play.api.mvc._
import play.api.test._
import play.api.test.Helpers._

class ExampleControllerSpec extends PlaySpec with Results {

  class TestController() extends Controller with ExampleController

  "Example Page#index" should {
    "should be valid" in {
      val controller = new TestController()
      val result: Future[Result] = controller.index().apply(FakeRequest())
      val bodyText: String = contentAsString(result)
      bodyText mustBe "ok"
    }
  }
}

JSON ボディなどの POST リクエストのテストを行う場合、上述のパターン (apply(fakeRequest)) は使用できません。そのかわり、 testControllercall() メソッドを使用してください。

trait WithControllerAndRequest {
  val testController = new Controller with ApiController

  def fakeRequest(method: String = "GET", route: String = "/") = FakeRequest(method, route)
    .withHeaders(
      ("Date", "2014-10-05T22:00:00"),
      ("Authorization", "username=bob;hash=foobar==")
  )
}

"REST API" should {
  "create a new user" in new WithControllerAndRequest {
    val request = fakeRequest("POST", "/user").withJsonBody(Json.parse(
      s"""{"first_name": "Alice",
        |  "last_name": "Doe",
        |  "credentials": {
        |    "username": "alice",
        |    "password": "secret"
        |  }
        |}""".stripMargin))
    val apiResult = call(testController.createUser, request)
    status(apiResult) mustEqual CREATED
    val jsonResult = contentAsJson(apiResult)
    ObjectId.isValid((jsonResult \ "id").as[String]) mustBe true

    // now get the real thing from the DB and check it was created with the correct values:
    val newbie = Dao().findByUsername("alice").get
    newbie.id.get.toString mustEqual (jsonResult \ "id").as[String]
    newbie.firstName mustEqual "Alice"
  }
}

§EssentialAction のテスト

ActionFilter のテストには EssentialAction のテストが必要になることがあります。 (EssentialAction の詳細はこちら)

そのためには、以下のように Helpers.call テストを使用します。

class ExampleEssentialActionSpec extends PlaySpec {

  "An essential action" should {
    "can parse a JSON body" in {
      val action: EssentialAction = Action { request =>
        val value = (request.body.asJson.get \ "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"
    }
  }
}

Next: ScalaTest による機能テスト


このドキュメントの翻訳は Play チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。