@JvmOverloadsなfunctionをmockできないケースがある
@JvmOverloads
を付与したKotlinのfunctionを、Javaから呼び出すコードを書いた。ここのテストを書こうと思ってmockito-kotlinでmockしようとしてできなかったときのメモ。ちなみにmockしたいクラスはDagger2でDIしている前提。
やりたかったこと
例えばこんなKotlinで書かれたfunctionがあったとして、
// Hoge.kt class Hoge { @JvmOverloads fun equal(a: String, b: String = "0"): Boolean = a == b }
それをJavaから呼び出しをするコードに対して
// Fuga.java public class Fuga { @Inject Hoge hoge = new Hoge(); @VisibleForTesting public Fuga(Hoge hoge) { this.hoge = hoge; } boolean callHogeEquals() { return hoge.equal("1"); } }
mockするようなテストコードを書こうと思ったらfailする
class FugaTest { @Mock lateinit var hoge: Hoge @Before fun setUp() { MockitoAnnotations.initMocks(this) } @Test fun testCallHogeEquals() { val fuga = Fuga(hoge) whenever(hoge.equal("1")).thenReturn(true) val result = fuga.callHogeEquals() Assert.assertEquals(true, result) } }
junit.framework.AssertionFailedError: Expected :true Actual :false
mockできずに本体コード通っちゃったのかな?と思ったが、こういうコードに対して同じようなテストを書いたところ
@JvmOverloads fun returnConcatString(a: String, b: String = "0") = a + b
@Test fun testCallHogeConcatString() { val fuga = Fuga(hoge) whenever(hoge.returnConcatString("1")).thenReturn("hoge") val result = fuga.callHogeCancatString() Assert.assertEquals("hoge", result) }
junit.framework.ComparisonFailure: Expected :hoge Actual :null
という結果になったので、どうやら本体コードを通っているのではなく、その型のdefault値が返って来ているように見える。
どうやって回避する?
1. 全てKotlinで書く
今回のようにTest対象のクラスだけがJavaで、mock対象とTestコードがKotlinで書かれた場合にこの問題が発生する。Test対象のクラスもKotlinで書いた場合には再現しないので、Test対象のクラスをKotlinで書き直すと回避できる。
2. 全てのParameterを指定する
別の方法として、Javaで書かれたTest対象のクラスからmock対象のコードを呼び出す時に、全てのParameterを指定するように変更するとこの問題は回避できる。
先ほどの例でいうと、Test対象のJavaをこんな感じに変更して、
boolean callHogeEquals() { return hoge.equal("1", "0"); // default parameterと同じものを2nd parameterに指定 }
Testコードも2nd paramterをつけてやると
@Test fun testCallHogeEquals() { val fuga = Fuga(hoge) whenever(hoge.equal("1", "0")).thenReturn(true) val result = fuga.callHogeEquals() Assert.assertEquals(true, result) }
無事にtestがpassする。ちなみにmock対象のコードは@JvmOverloads
をつけたままで問題ない。
なぜmockできないのか?
@JvmOverloads
がついたfunctionをJavaのbyte codeで見てみると、それぞれのシグネチャのパターン分のメソッドだけでなく、ブリッジメソッドも作られていた。
// access flags 0x1049 public static synthetic bridge equal$default(Lcom/example/muumuu/playgroundapp/Hoge;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Z @Lkotlin/jvm/JvmOverloads;() // invisible ILOAD 3 ICONST_2 IAND IFEQ L0 L1 LINENUMBER 8 L1 LDC "0" ASTORE 2 L0 ALOAD 0 ALOAD 1 ALOAD 2 INVOKEVIRTUAL com/example/muumuu/playgroundapp/Hoge.equal (Ljava/lang/String;Ljava/lang/String;)Z IRETURN MAXSTACK = 3 MAXLOCALS = 5
で、mockする箇所で引数一つで呼び出すとそのブリッジメソッドをINVOKESTATICしている。
L3 LDC "1" ACONST_NULL ICONST_2 ACONST_NULL INVOKESTATIC com/example/muumuu/playgroundapp/Hoge.equal$default (Lcom/example/muumuu/playgroundapp/Hoge;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Z INVOKESTATIC java/lang/Boolean.valueOf (Z)Ljava/lang/Boolean; INVOKESTATIC com/nhaarman/mockito_kotlin/MockitoKt.whenever (Ljava/lang/Object;)Lorg/mockito/stubbing/OngoingStubbing; ICONST_1 INVOKESTATIC java/lang/Boolean.valueOf (Z)Ljava/lang/Boolean; INVOKEINTERFACE org/mockito/stubbing/OngoingStubbing.thenReturn (Ljava/lang/Object;)Lorg/mockito/stubbing/OngoingStubbing; POP
自分の理解ではmockitoは@Mock
を付与したインスタンスはdummy objectを生成して、when等で定めた振る舞いをstubとして生成している。そのため、今回のようにブリッジメソッドをstubとして生成しても実際にJavaのコードからコールされるのはこのブリッジメソッドではないのでstubが存在しないため戻り値のクラスのデフォルト値が返ってしまったようだ。
それでは上記回避策をとった場合は、どのようなbyte codeが生成されていて、なぜ問題が起きないのか見ていこう。
まず、全てKotlinで書かれた場合、mockする時だけでなく本体側も@JvmOverloads
なfunctionを同じシグネチャでコールするとブリッジメソッドが呼ばれるので、stubする対象が一致するので正常にmockできる。
次にstub生成時に引数をフルで指定する場合は、ブリッジメソッドではなく引数がフルに揃ったシグネチャのメソッドのスタブが作成されるため正常にmockできる。
L3 LDC "1" LDC "1" INVOKEVIRTUAL com/example/muumuu/playgroundapp/Hoge.equal (Ljava/lang/String;Ljava/lang/String;)Z INVOKESTATIC java/lang/Boolean.valueOf (Z)Ljava/lang/Boolean; INVOKESTATIC com/nhaarman/mockito_kotlin/MockitoKt.whenever (Ljava/lang/Object;)Lorg/mockito/stubbing/OngoingStubbing; ICONST_1 INVOKESTATIC java/lang/Boolean.valueOf (Z)Ljava/lang/Boolean; INVOKEINTERFACE org/mockito/stubbing/OngoingStubbing.thenReturn (Ljava/lang/Object;)Lorg/mockito/stubbing/OngoingStubbing; POP
はー、フルKotlinな世界はよ〜〜〜〜〜