Play FrameworkではGoogle Guiceが標準で採用されている。
統合テストを書いているとGuiceが生成するインスタンスをspyに差し替えたくなる時がごく稀にある。
この場合、Injector.getInstance()
が返したインスタンスをspyにするのではなく、Injector.getInstance()
のインスタンスを生成する処理の中でspyにしてやらないと、テスト対象の依存関係の中にspyが組み込まれない。
Injectorがインスタンスを生成する処理をカスタマイズしたい場合、インスタンスの生成手順をメソッドで定義する@Provides Methodsや、インスタンスを生成するメソッドを持つクラスを定義するProviderが使える。 @Provides Methodsを使った場合は本番用のコードの中にテストのためのコードを追加しなければならなくなるため、今回の目的ならテストコードを書くディレクトリでProviderを定義してやる方がいいだろう。
package com.pkinop.example import com.google.inject.Injector import org.mockito.ArgumentMatchers.any import org.mockito.Mockito.{doAnswer, spy, times, verify} import org.mockito.invocation.InvocationOnMock import org.mockito.stubbing.Answer import org.scalatest.TestData import org.scalatestplus.play.PlaySpec import org.scalatestplus.play.guice.GuiceOneAppPerTest import play.api.Application import play.api.inject.bind import play.api.inject.guice.GuiceApplicationBuilder import javax.inject.{Inject, Provider, Singleton} trait Reverser { def reverse(x: Int): Int } class ReverserImpl @Inject()() extends Reverser { def reverse(x: Int): Int = -x } class ReverserProvider @Inject()(injector: Injector) extends Provider[Reverser] { override def get(): Reverser = spy( injector.getInstance(classOf[ReverserImpl]) ) } class ReverserClient @Inject()(reverser: Reverser) { def reverse(x: Int): Int = reverser.reverse(x) } class ReverserSpec extends PlaySpec with GuiceOneAppPerTest { override def newAppForTest(testData: TestData): Application = GuiceApplicationBuilder().overrides( // SingletonにしておかないとReverserを要求する場所ごとに別のspyが生成されてしまい // 動作の変更や呼び出しの検証を行うspyが依存関係の解決に使われなくなってしまう bind[Reverser] .toProvider(classOf[ReverserProvider]) .in[Singleton] ).build() "呼び出しをテストする" in { val reverserClient = app.injector.instanceOf[ReverserClient] val result = reverserClient.reverse(1) result mustBe -1 val reverser = app.injector.instanceOf[Reverser] verify(reverser, times(1)) .reverse(1) } "実装を変更してみる" in { val reverser = app.injector.instanceOf[Reverser] doAnswer(new Answer[Int] { override def answer(invocation: InvocationOnMock): Int = { val original = invocation.callRealMethod().asInstanceOf[Int] original * 10 } }).when(reverser).reverse(any) val reverserClient = app.injector.instanceOf[ReverserClient] val result = reverserClient.reverse(1) result mustBe -10 } }
実際にReverserClient
がReverser
のインスタンスを要求すると、ReverserImpl
のspyが依存関係の解決に使われているのがわかるだろう。
ProviderもGuiceで生成でき、Providerの中でもGuiceによる依存関係の解決ができるというのがミソだ。
なおこの方法では具象クラスのインスタンスを要求している箇所でspyに差し替えることはできない。
ここでわざわざReverser
とReverserImpl
を用意しているのもそれが理由だ。
仮にReverser
というクラスを用意してそのまま実装を書くと、以下のようになる。
class Reverser @Inject()() extends Reverser { def reverse(x: Int): Int = -x } class ReverserProvider @Inject()(injector: Injector) extends Provider[Reverser] { override def get(): Reverser = spy( injector.getInstance(classOf[Reverser]) ) }
すると、GuiceはReverser
のインスタンスを生成するためにReverserProvider.get()
を呼び出すが、その中ではGuiceを使ってReverser
のインスタンスを生成しているため循環参照に陥ってしまう。
もっとも、spyに差し替えたくなるようなクラスはインターフェイスが用意されていることがほとんどだろうし、そうでなくてもインターフェイスを用意するだけであれば本番用のコードの中にテスト用の実装が入るわけではないから、テストのために本番コードを歪める行為の中ではだいぶマシな部類であり、この制限が問題になることはあまり無いだろう。