§ScalaTest を使用したアプリケーションのテスト
アプリケーションのテストを作成するのは複雑な作業となりがちです。Play はヘルパーやスタブを提供し、ScalaTest は統合ライブラリ「 ScalaTest + Play 」を提供しており、テストの作成をできる限り容易にしています。
§概要
テストのソースファイルは “test” フォルダに配置します。
テストは Play のコンソールから実行できます。
test
を実行すると全てのテストが実行されます。test-only
を実行すると、その後に続くクラス名のテストクラスのみが実行されます。 例)test-only my.namespace.MySpec
- 失敗したテストだけ走らせたい場合は、
test-quick
を実行します。 - 継続的にテストを走らせたい場合は、実行するテストコマンドの前にチルダをつけます。 例)
~test-quick
FakeApplication
などのヘルパーにアクセスしたい場合は、test:console
を実行します。
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 IDE と ScalaTest 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)
) は使用できません。そのかわり、 testController
で call()
メソッドを使用してください。
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 のテスト
Action
や Filter
のテストには 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 チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。