§specs2 を使用したアプリケーションのテスト
アプリケーションのテストを作成するのは複雑な作業となりがちです。Play は標準のテストフレームワークを提供しており、テストの作成をできる限り容易にするヘルパーやスタブを提供しています。
§概要
テストのソースファイルは “test” フォルダに配置します。テンプレートとして使えるサンプルファイルが二つ、 test フォルダに配置してあります。
テストは Play のコンソールから実行できます。
test
を実行すると全てのテストが実行されます。test-only
を実行すると、その後に続くクラス名のテストクラスのみが実行されます。 例)test-only my.namespace.MySpec
- 失敗したテストだけ走らせたい場合は、
test-quick
を実行します。 - 継続的にテストを走らせたい場合は、実行するテストコマンドの前にチルダをつけます。 例)
~test-quick
FakeApplication
などのヘルパーにアクセスしたい場合は、test:console
を実行します。
Play のテストは SBT に基づいており、 testing SBT に詳細が記載されています。
§specs2 を使う
Play の specs2 サポートを使用するには、Play の specs2 への依存をテストスコープの依存性としてビルドに追加します。
libraryDependencies += specs2 % Test
specs2 のテストは、様々なコードパスでテスト対象システムを動作させるサンプル値を含んだ specifications にまとめられます。
specification は 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")
}
}
}
specification は Scala plugin を使用した IntelliJ IDEA あるいは Scala IDE を使用した Eclipse で実行可能です。詳しくは IDE のページを参照してください。
NOTE: presentation compiler のバグで、 Eclipse でコーディングする場合、特定のフォーマットで書く必要があります。
- パッケージはディレクトリパスと同じでなければいけません。
- specification は、
@RunWith(classOf[JUnitRunner])
でアノテーションされなければいけません。
Eclipse では以下のように書きます。
package models // このファイルは、"models" というディレクトリに配置されている必要があります。
import org.specs2.mutable._
import org.specs2.runner._
import org.junit.runner._
@RunWith(classOf[JUnitRunner])
class ApplicationSpec extends Specification {
...
}
§Matchers
サンプル値を使うと、サンプル値の結果を返さなければいけません。通例、must
という宣言がよく使われます。
"Hello world" must endWith("world")
must
のあとの表現は、 matchers
と呼ばれます。matchers は、Success や Failure など、サンプル値の結果を返します。結果を返さない場合、サンプル値はコンパイルされません。
最も便利な matchers は、match results です。同一性の検査や、 Option や Either の結果を判定、例外が投げられたかどうかの検査に使われます。
他にも XML や JSON を比較するテストの optional matchers などもあります。
§Mockito
モックは外部の依存関係から独立したユニットテストを行う際に用います。例えば、クラスが外部の DataService
クラスに依存する場合、 DataService
のオブジェクトをインスタンス化しなくても、モックを使って適切なデータを提供できます。
Mockito は、標準のモックライブラリとして、 specs2 に組み込まれています。
Mockito は以下のように使います。
import org.specs2.mock._
Mockito を使えば、クラスへの参照を次のようにモックに向けることができます。
trait DataService {
def findData: Data
}
case class Data(retrievalDate: java.util.Date)
import org.specs2.mock._
import org.specs2.mutable._
import java.util._
class ExampleMockitoSpec extends Specification with Mockito {
"MyService#isDailyData" should {
"return true if the data is from today" in {
val mockDataService = mock[DataService]
mockDataService.findData returns Data(retrievalDate = new java.util.Date())
val myService = new MyService() {
override def dataService = mockDataService
}
val actual = myService.isDailyData
actual must equalTo(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
メソッドをテストすることができます
object UserServiceSpec extends Specification with Mockito {
"UserService#isAdmin" should {
"be true when the role is admin" in {
val userRepository = mock[UserRepository]
userRepository.roles(any[User]) returns Set(Role("ADMIN"))
val userService = new UserService(userRepository)
val actual = userService.isAdmin(User("11", "Steve", "[email protected]"))
actual must beTrue
}
}
}
§コントローラのユニットテスト
コントローラを object として定義すると、ユニットテストするのが難しくなります。Play では 依存性注入 で緩和できます。あるいは、コントローラに 明示的に型付けられた自己参照 のトレイトを使用するという方法によっても、object として定義されたコントローラのユニットテストを幾分楽にすることができます。
trait ExampleController {
this: Controller =>
def index() = Action {
Ok("ok")
}
}
object ExampleController extends Controller with ExampleController
そして、トレイトのテストを追加します。
import play.api.mvc._
import play.api.test._
import scala.concurrent.Future
object ExampleControllerSpec extends PlaySpecification 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 must be equalTo "ok"
}
}
}
§EssentialAction のテスト
Action
や Filter
のテストには EssentialAction
のテストが必要になることがあります (EssentialAction の詳細はこちら)。
そのためには、以下のように Helpers.call
テストを使用します。
object ExampleEssentialActionSpec extends PlaySpecification {
"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: Specs2 による機能テスト
このドキュメントの翻訳は Play チームによってメンテナンスされているものではありません。 間違いを見つけた場合、このページのソースコードを ここ で確認することができます。 ドキュメントガイドライン を読んで、お気軽にプルリクエストを送ってください。