JDBな人生  専門的なことから日常的なことまで~ まぁ自由きままに書いていきます。
2017年05月 / 04月<< 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 >>06月

アクセスランキング

[ジャンルランキング]
コンピュータ
413位
アクセスランキングを見る>>

[サブジャンルランキング]
プログラミング
58位
アクセスランキングを見る>>

Kotlin + Mockitoでany<T>()やeq<T>()を使いたい

今年頭から、APIの開発をMojolicious/PerlからSpring Boot/Kotlinに移行しています。今回は初のKotlinの記事です。

KotlinはJava完全互換を謳っているので、当然Java向けのモックライブラリであるMockitoもきちんと動くということになるはずです。しかし、実際には以下のような状況ではうまく動きません。

java - Is it possible to use Mockito in Kotlin? - Stack Overflow
http://stackoverflow.com/questions/30305217/is-it-possible-to-use-mockito-in-kotlin

まずモック対象のメソッドがこんな感じだとします。
ここでのポイントは引数の`String`がNon-Nullになっていることです。
class UserService{ 
    fun findUserByToken(token: String) {
    // 定義
    }
}

そしてモックを実行します。「特定の値が指定されたとき」であれば次の書き方ができます。
Mockito.doReturn(dummyUser).`when`(userServiceMock)
    .findUserByToken("token string")
Mockito.doReturn(dummyUser).`when`(userServiceMock)
    .findUserByToken(Mockito.eq("token string"))

条件を特に指定しない場合、次の書き方ができます。
Mockito.doReturn(dummyUser).`when`(userServiceMock)
    .findUserByToken(Mockito.anyString())
Mockito.doReturn(dummyUser).`when`(userServiceMock)
    .findUserByToken(Mockito.any())
Mockito.doReturn(dummyUser).`when`(userServiceMock)
    .findUserByToken(Mockito.anyObject())

さて、この記事にたどり着いた人ならお分かりかと思いますが、これらの5つの記法のうち、実際に動くのは最初の一つのみです。
こんな例外が発生します。
java.lang.IllegalStateException: Mockito.any() must not be null

その原因はMockitoの実装にあります。MockitoのSourceを見ればわかるのですが、`any()`や`anyObject()`を呼ぶと何が起きるのかというと、
①内部で条件の登録を処理する
②メソッドの戻り値としては`null`を返す
という具合です。
https://github.com/mockito/mockito/blob/master/src/main/java/org/mockito/ArgumentMatchers.java
public static <T> T any() {
    return anyObject();
}

public static <T> T anyObject() {
    reportMatcher(Any.ANY);
    return null;
}

この戻り値`null`はどこに行くかというと、mockされた`findUserByToken(token: String)`に行きます。が、問題はこのtokenはNon-NullなStringだということです。親切なKotlinは、このようなNon-Nullな引数を持つメソッドを呼び出すときに、事前に「渡す予定の」引数がnullかどうかチェックしてくれます。

つまり、テストコードがKotlinのSourceからJavaのコードにコンパイルされるときに、このようなメソッド呼び出しは
①引数として渡す予定の値がnullかどうかチェックする、nullなら例外を投げる
②実際にメソッドを呼び出す
という2段階の処理として生成されるということです。KotlinのNon-Nullの実装の機構がそうなっている、という話ですね。
そのため、any()の戻り値がmockされたuserServiceのインスタンスに届く前に、Kotlinのnullチェックに引っ掛かってしまい、敢え無く例外で落ちる、ということになるわけです。

解決策は、このnullチェックを何とかしてくぐり抜ければ良いということになりますが、そのための方法が、「総称型扱いでnullを渡す」というものです。この場合だと、「なぜか」上記のコンパイル時の処理で①(渡す引数のnullチェック)が省略されます。Kotlinの実装依存の解決策といえばそう、ということになりますね。
fun <T> nullNotNull(): T {
    return null as T
}

// 落ちる
userServiceMock.findByToken(null);

// 落ちる
userServiceMock.findByToken(null as String);

// 落ちない
userServiceMock.findByToken(nullNotNull());
※Kotlinでは「渡す予定の引数のnullチェック」と「引数としてもらった変数のnullチェック」の両方を行うので、これがmockでなければ三つとも落ちます。ここではMockitoが中身をオーバライドするので、引数としてもらった変数のチェックは実行されません。

一般的な解決策としては、次のようなclassを作っておいて、Mockito.any()やMockito.eq()の代わりに使えばなんとかなります。
class KotlinMockitoHelper {
    companion object {
        fun <T> any(): T {
            return Mockito.any()
                    ?: null as T
        }

        fun <T> eq(value: T): T {
            return if (value != null)
                Mockito.eq(value)
            else
                null
                        ?: null as T
        }
    }
}

なんだかすっきりしない結論ですね…。
とりあえず、Stack Overflowで出ていた解決策でどうして解決できるんだ??と調べてみた次第です。改めてポイントをまとめると、
①Mockito.any()の戻り値はnullである、
②<T>に対してはnullチェックをサボる、というところです。
これだと、今後の実装でまた動かなくなる、ということもあり得ると思います。まあ、モックするというテスト手法は下火になりつつあるという話もあるので、モックせずにうまく書いていく、という方針をとるのも手なのかもしれません。
   Kotlin    TB(0)    CM(0)    EDIT    ページ↑

プロフィール

JDB Luigi

Author:JDB Luigi
どこにでもいるようなありふれた人間・・・という訳でもなく、かと言って怪しい宗教を信仰する変人という訳でも無い。

基本的に掲載しているコード等は煮ていただいても焼いていただいても結構ですが、利用は自己責任にてお願いいします。
また、バグ・アドバイス等もしあればよろしくお願いします。