Guiceが生成するインスタンスをMockitoでspyする

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

実際にReverserClientReverserインスタンスを要求すると、ReverserImplspyが依存関係の解決に使われているのがわかるだろう。 ProviderもGuiceで生成でき、Providerの中でもGuiceによる依存関係の解決ができるというのがミソだ。

なおこの方法では具象クラスのインスタンスを要求している箇所でspyに差し替えることはできない。 ここでわざわざReverserReverserImplを用意しているのもそれが理由だ。 仮に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])
  )
}

すると、GuiceReverserインスタンスを生成するためにReverserProvider.get()を呼び出すが、その中ではGuiceを使ってReverserインスタンスを生成しているため循環参照に陥ってしまう。

もっとも、spyに差し替えたくなるようなクラスはインターフェイスが用意されていることがほとんどだろうし、そうでなくてもインターフェイスを用意するだけであれば本番用のコードの中にテスト用の実装が入るわけではないから、テストのために本番コードを歪める行為の中ではだいぶマシな部類であり、この制限が問題になることはあまり無いだろう。