Play Framework 2.4 Scala + Specs2 + Mockito + Guice DI でテスト素人がテストに挑戦した話

Play Framework 2.4 をちまちま触っているのですが、モダンな開発といえば自動テストはかかせないよな〜〜〜〜〜〜〜と思いつつずっと出来てなかったので触りました。

案の定、いろんなことに引っかかったし、とくに Play 2.4 から本格導入された Guice による動的DIとかと絡んで、どうすれバインダーってなってたし、モックとか Mockito とか聞いたことあるけど、どうやったらいいんですかね〜ってなったし、Futureってどう評価したらいいんだ…とかとか、いろいろあるのでまとめました。

慣れればできそうなので、テストとか Play + Specs2 + Mockito はあんまり、みたいな人、自分みたいな初心者に役に立てばいいなぁ。

シナリオ

以下のModel層のServiceクラス MailServiceImpl についてテストしたいとする。簡単のために confirmMail(user: User, mailToken: MailToken) については信頼されているものとして、sendConfirm(user: User) だけテストする。

package models.services

import javax.inject.Inject

import models.daos.MailTokenDAO
import models.{MailToken, User}
import play.api.i18n.{I18nSupport, Messages, MessagesApi}
import play.api.libs.mailer.{Email, MailerClient}

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

class MailServiceImpl @Inject() (
  mailerClient: MailerClient,
  val messagesApi: MessagesApi,
  mailTokenDAO: MailTokenDAO) extends MailService with I18nSupport {

  def sendConfirm(user: User): Future[String] = {
    val mailToken = MailToken.create(user, "confirm")
    mailTokenDAO.create(mailToken)
    Future(mailerClient.send(confirmMail(user, mailToken)))
  }

  def confirmMail(user: User, mailToken: MailToken): Email = ???
}

話を簡単にするためにクラスも簡単にしてるヨ。

Test のファイルをつくる

Play の Project をつくるとデフォルトで Test ディレクトリがあるので、その配下にテストクラスを配置する。Controller のテストは難しそうだったので、Service的な何かをテストすることにする。

ProjectRoot
├ …
└Test
  ├controllers
  │ └ApplicationControllerSpec.scala
  └services
    └MailServiceSpec.scala ←今回追加してみたやつ

Test の基本をつくる

Play 標準のテストツール Specs2 での基本的な準備をする。

package services

import org.specs2.mock.Mockito
import play.api.test._
import play.api.test.Helpers._

class MailServiceSpec extends PlaySpecification with Mockito {
}

赤字のところが Specs2 と Mockito を利用した Test に必要な部分。重要なのは Test クラスに PlaySpecification と Mockito トレイトをミックスインすること。ちなみに PlaySpecification は play.api.test パッケージに含まれていて、Specs2 のテスト用トレイト Specification を拡張している。

とりあえずこれでテンプレート的な基本準備が整った

テストしたい対象とテスト項目を should 〜 in で記述する

いまさらながら、テストはテストしたい対象(普通はメソッド呼び出し)がさまざまな入力値(テストケース)に対して、想定した振る舞いをするかを検査するものとうことです。で、Specs2 ではこのテストしたい対象とテストケースを should 〜 in という DSL (Domain Specific Language) で記述する。

package services

import org.specs2.mock.Mockito
import play.api.test._
import play.api.test.Helpers._

class MailServiceSpec extends PlaySpecification with Mockito {
  "MailService#sendConfirm" should {
    "send an email for testUser" in {
      ???
    }
  }
}

こんな感じ。??? のところに具体的なテストケースを記述する。ちょっと見た目からいかつさが出てきた。

見ての通り、MailService クラスの sendConfirm メソッドを対象にしたテストを書きます。具体的なテストケース設計はいろいろ難しいお話が多そうなので他に譲って、今回はとりあえず正しいデータが与えられた時に正しくメールを送れる成功テストケースをつくります。

メソッドのテストに必要な環境を準備する

メソッドに渡さなきゃいけない引数はもちろんのこと、そのメソッドを持つクラスのインスタンス化に必要なデータも用意しなきゃいけない(当たり前)。こういう時に依存関係の少ない疎なモジュール設計にしようね!!!!!!!!!!!!!と思うのでテスト重要。

Mock【モック】とは

メソッドのテストに必要な環境を準備するとして、User みたいな model は簡単にインスタンスが作れるし副作用とかないので問題はないんだけれど、例えば DAO みたいなのだと、テストのたびに実際に DB に書き込んだり、読み込み用のデータを毎回 DB にセットしておかなきゃいけないのはなかなかにしんどい。ということで Mock という考え方がある。

名前の通り Mock = 見かけ上そのインスタンスにみえるもの・偽物 ということで、実際のインスタンスをつくる代わりにそのインスタンスの振る舞いを与えて、見かけ上そのインスタンスにみえる偽物を引数や環境として与えることによって、毎回実際に DB アクセスしないで済むようにしたり、処理時間のかかる部分をスキップしたり、まだ未実装の依存先を代替して独立してテストを行えるようにする(まさにユニットテスト、という感じ)ためのもの。

そしてその Mock をお手軽につくらせてくれるのが Mockito というわけ。早速 Mockito を使って環境を準備する。

メソッドの引数、テスト対象クラスのコンストラクタ引数を準備する

はい。

package services

import java.util.UUID

import com.mohiva.play.silhouette.api.LoginInfo
import models.{MailToken, User}
import models.daos.MailTokenDAO
import reactivemongo.api.commands.WriteResult
import org.specs2.mock.Mockito
import play.api.test._
import play.api.test.Helpers._

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global

class MailServiceSpec extends PlaySpecification with Mockito {
  "Mailservice#sendConfirm" should {
    "send an email for testUser" in {
      val testUser = User(UUID.randomUUID(),
                          LoginInfo("email", "hoge@piyo.com"),
                          Option("Test"), Option("Taro"),
                          Option("Test Taro"),
                          Option("hoge@piyo.com"),
                          None)
      val mailTokenDAOMock = mock[MailTokenDAO]
      mailTokenDAOMock.create(any[MailToken]) returns Future(mock[WriteResult])
    }
  }
}

import はぶっちゃけ IDE 使ってれば勝手に挿入してくれると思うので気にしなくていいです。

testUser はわかりやすいですね。sendConfirm(user: User) の引数に必要な User のインスタンスを作っています。中身はどうでもいいです(テストしたい内容による)。

重要なのが mailTokenDAOMock で、ここで Mockito をつかって Mock オブジェクトを作っています。さらにそのあと、メソッド呼び出しの形式 + returns + 返したい値 で振る舞いを定義しています。今回は mailTokenDAOMock の create メソッドに対して、どんな MailToken 型の値を入れても Future[WriteResult] 型の値 Future(mock[WriteResult]) を返すように定義しました。

引数は限定することもできて、入力が null の時は null を返すとかもできます。

mailTokenDAOMock.create(null) returns null

だいたいこんな感じでテストケースの環境を準備していきます。

※はまりポイント1  mock[Type]

なんか見た目上 mock って Mock オブジェクトの apply メソッドっぽいじゃないですか。でも実際は MockCreation トレイトを拡張した Mockito トレイトのメソッドです。最初 Mock[MailTokenDAO] って書いて動かなくて悩みましたね…(mは小文字が正しい)

※はまりポイント2  Guice の DI を利用しながらテスト対象のクラスをインスタンス化する

さて、メソッドの引数、テスト対象クラスのコンストラクタの引数を用意すると言いましたが、まだ足りてないです。コンストラクタ引数で DI されている mailerClient と messagesApi がない。

愚直にインスタンスを作ってもいいんですが、別にテスト対象でもないし、テストケースに関わらないこういったライブラリのクラスとかは手を抜きたいですよね。実際にインスタンス化されるクラスの実装を調べるのもめんどうだし。

しかし、Specs2 のテストでは Guice が動いてくれないので、下記のようなテストは Can’t find a constructor for class ~ とエラーを吐いて死にます。

// 思いつくけどダメな例
class MailServiceSpec @Inject() (val mailerClient: MailerClient, val messagesApi: MessagesApi)
  extends PlaySpecification with Mockito {
  "Mailservice#sendConfirm" should {
    "send an email for testUser" in {
      val testUser = User(UUID.randomUUID(),
                          LoginInfo("email", "hoge@piyo.com"),
                          Option("Test"), Option("Taro"),
                          Option("Test Taro"),
                          Option("hoge@piyo.com"),
                          None)
      val mailTokenDAOMock = mock[MailTokenDAO]
      mailTokenDAOMock.create(any[MailToken]) returns Future(mock[WriteResult])

      // mailerClient と messagesApi を使う
      ...
    }
  }
}

さてどうするか。できれば手は抜きたい。

実際には、play.api.inject.guice.GuiceApplicationBuilder を使うことで解決できます。

package services

import java.util.UUID

import com.mohiva.play.silhouette.api.LoginInfo
import models.{MailToken, User}
import models.daos.MailTokenDAO
import models.services.MailServiceImpl
import play.api.i18n.MessagesApi
import play.api.inject.guice.GuiceApplicationBuilder
import play.api.libs.mailer.MailerClient
import reactivemongo.api.commands.WriteResult
import org.specs2.mock.Mockito
import play.api.test._
import play.api.test.Helpers._

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.reflect.ClassTag

class MailServiceSpec extends PlaySpecification with Mockito with Inject {
  lazy val mailerClient = inject[MailerClient]
  lazy val messagesApi = inject[MessagesApi]

  "MailService#sendConfirm" should {
    "send an email for testUser" in {
      val mailTokenDAOMock = mock[MailTokenDAO]
      mailTokenDAOMock.create(any[MailToken]) returns Future(mock[WriteResult])

      val testUser = User(UUID.randomUUID(),
                          LoginInfo("email", "hoge@piyo.com"),
                          Option("Test"), Option("Taro"),
                          Option("Test Taro"),
                          Option("hoge@piyo.com"),
                          None)

      val mailServiceTest = new MailServiceImpl(mailerClient, messagesApi, mailTokenDAOMock)
    }
  }
}

trait Inject {
  lazy val injector = (new GuiceApplicationBuilder).injector()

  def inject[T : ClassTag]: T = injector.instanceOf[T]
}

Inject トレイトをつくって、GuiceApplicationBuilder から injector を取得し、その injector を利用して DI を行います。実際に注入される依存性については考えなくてもオッケー!Happy.

というわけでテスト用のクラスもできて環境が整いました。ここまでが長かった。

メソッドのテストを書く

最後に、テストケースとしてメソッドを呼び出し、評価します。

package services

import java.util.UUID

import com.mohiva.play.silhouette.api.LoginInfo
import models.{MailToken, User}
import models.daos.MailTokenDAO
import models.services.MailServiceImpl
import play.api.i18n.MessagesApi
import play.api.inject.guice.GuiceApplicationBuilder
import play.api.libs.mailer.MailerClient
import reactivemongo.api.commands.WriteResult
import org.specs2.mock.Mockito
import play.api.test._
import play.api.test.Helpers._

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import scala.reflect.ClassTag

class MailServiceSpec extends PlaySpecification with Mockito with Inject {
  lazy val mailerClient = inject[MailerClient]
  lazy val messagesApi = inject[MessagesApi]

  "MailService#sendConfirm" should {
    "send an email for testUser" in {
      val mailTokenDAOMock = mock[MailTokenDAO]
      mailTokenDAOMock.create(any[MailToken]) returns Future(mock[WriteResult])

      val testUser = User(UUID.randomUUID(),
                          LoginInfo("email", "hoge@piyo.com"),
                          Option("Test"), Option("Taro"),
                          Option("Test Taro"),
                          Option("hoge@piyo.com"),
                          None)

      val mailServiceTest = new MailServiceImpl(mailerClient, messagesApi, mailTokenDAOMock)

      val ret = await(mailServiceTest.sendConfirm(testUser))

      ret must beMatching("")
    }
  }
}

trait Inject {
  lazy val injector = (new GuiceApplicationBuilder).injector()

  def inject[T : ClassTag]: T = injector.instanceOf[T]
}

これで一応完成形です。

評価値 ret をマッチャーという Assertion みたいなものを使って期待値と比較します。マッチャーは型とかによってたくさん種類があるけど、それは調べてみてください(丸投げ)。この最後のマッチャーのところがまさにテストで確かめたいことになります。

※はまりポイント3  Future の値ってどう評価するの

サラッと流しましたが、上のテストでは評価値 ret を得るために mailServiceTest.sendConfirm(testUser) を await というメソッドの引数に渡しています。ちなみにこの await は play.api.test.FutureAwaits のメソッドです。

なにが問題かというと、Future の値は決定前に直接取り出せないので、決定するのを待って評価しなければならないということ。つまり Future を直接マッチャーに渡すのはできないんですよね。なので、Future を返すメソッドの場合はそれを await メソッドで包んでやって、評価値を確定させてマッチャーに渡せば良い、ということ。

ちなみにググると Mathcer.await メソッドを使え、すなわち

mailServiceTest.sendConfirm(testUser) must beMatching("").await

みたいに書いてある場合があるけど、どうも情報が古いのか現在の Matcher に await メソッドは生えてなかった。くそぅ。

テストを実行する

エライ長くなってしまいましたが、いよいよテストの実行です。Play のプロジェクトルートで activator test コマンドを実行するか、activator ui で GUI を起動して、そこの Test セクションから実行することもできます。

スクリーンショット 2016-04-20 17.13.32

テストが通ったヤッター!

詳細なテストの実行結果は、コマンドラインに吐かれているので、activator ui の場合は Build セクションを確認すると詳細が見れます。

というわけではまりポイントつき Play 2.4 + Specs2 + Mockito 弾丸ツアーでした。

テストできるようになってよかったね。

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中