Androidで使用するMockライブラリをmockito-kotlinに移行しました

N予備校Androidチームでは、Unit Testに使用しているMockライブラリをMockitoからmockito-kotlinに移行しました。 この記事では、ライブラリを移行した経緯、mockito-kotlinを選定した理由と移行して得られたメリットについて書きます。

Mockライブラリを移行した背景

私たちのチームでは積極的にコードのKotlin化を進めていて、2020年6月現在で約95%のコードがKotlinで書かれています。テストファイルだけを見れば、100%Kotlin化が完了している状態です。 その中でKotlinとの相性がよくないライブラリはとても書きにくくなり、その筆頭がMockitoでした。

例えばMockitoではモックしたライブラリからの返り値を設定する場合には"when"を使用しますが、Kotlinでは"when"は予約語なのでバッククオート(`)で囲わなければなりません。

// Java
when(mockClass.getFirst()).thenReturn(1)
 
// Kotlin
`when`(mockClass.first).thenReturn(1)  // whenは予約語なので`when`と書く

また、参照するときにMockitoをimportする書き方と、Mockito.whenをimportする書き方があり、人によって書き方がわかれてしまうのも面倒でした。

// Mockitoをimportする書き方
import org.mockito.Mockito
Mockito.`when`(mockClass.first).thenReturn(1) 
 
// Mockito.`when`をimportする書き方
import org.mockito.Mockito.`when`
`when`(mockClass.first).thenReturn(1)

そして、MockitoのMatcherであるany()やeq()をそのままKotlinで書くことはできず、Kotlinのテストのために自前で拡張関数を用意している状態でした。

// Kotlin で any を使えるようにする
fun <T> anyKt(): T {
    return Matchers.any<T>() ?: null as T
}
 
// Kotlinでeqを使えるようにする
fun <T> eqKt(value: T): T {
    return Matchers.eq<T>(value) ?: null as T
}

このような問題は取るに足らないようにも見えます。 しかし私たちのプロジェクトではテストの数が1,000を超えており、全てのテストをメンテナンスしていくためにはこのようなささいな問題のために大きな労力がかかってしまいます。

そこで、Kotlinに最適なMockライブラリに切り替えること決めました。

mockito-kotlinを選んだ理由

Kotlinで書きやすいMockライブラリとして検討したのはmockito-kotlinとMockKでした。 以下は選定時点(2020/04)にまとめた両者の比較です。

評価軸 MockK mockito-kotlin
作成者 OSSプロジェクト、メインはOleksiy Pylypenko OSSプロジェクト、メインはNiek Haarman(オランダのLabel305の社員)
Contributions 1,332commits, 74PR 360commits, 169PR
Star数 2,900 2,200
最終リリース 2019/3/25 (v1.9.3) 2019/9/8 (v2.2.0)
スポンサー 2社 + 7人 なし
特徴 Kotlinに特化して作成されたMockライブラリ。CoroutineやObject, private関数にも対応している。 MockitoをKotlinでも使いやすくした簡易ライブラリ。Mockitoに関する資料が多いので検索しやすい。
ドキュメント https://mockk.io/ https://github.com/nhaarman/mockito-kotlin/wiki/Mocking-and-verifying
ライセンス Apache License 2.0 MIT License

どちらのライブラリも、十分なContributionとStarがあり、最終リリースもそれほど古くありませんでした。 スポンサーの有無やドキュメントの充実性ではMockKのほうが良さそうでしたが、Mockitoからの書き換えという点ではmockito-kotlinのほうが導入・学習コストが低そうでした。

その中で最終的な決め手になったのは実行速度です。 実行速度を計測するために、以下のクラスに対してテストを書いてみました。

/**
 * storeの合計値が+だったらsnackbarを表示するクラス
 */
class SampleClass(
    private val store: StoreInterface,
    private val snackbar: SnackbarInterface
) {
    fun calc() {
        var answer = 0
        store.numbers.map { answer += it }
        if (answer > 0) snackbar.show()
    }
}

このクラスに対してmockito-kotlinで以下のようにテストを書きます。

class SampleClassTestByMockitoKotlin {
    private val store: StoreInterface = mock()
    private val snackbar: SnackbarInterface = mock()
 
    private val sampleClass = SampleClass(store, snackbar)
 
    @Test
    fun `storeの合計値が+だったらsnackbarが表示されること`() {
        whenever(store.numbers).thenReturn(listOf(1, 2))
 
        sampleClass.calc()
 
        verify(snackbar).show()
    }
 
    @Test
    fun `storeの合計値が0以下だったらsnackbarが表示されないこと`() {
        whenever(store.numbers).thenReturn(listOf(1, -1))
 
        sampleClass.calc()
 
        verify(snackbar, never()).show()
    }
}

また、MockKでもテストを書いてみます。

class SampleClassTestByMockK {
    private val store: StoreInterface = mockk()
    private val snackbar: SnackbarInterface = mockk(relaxUnitFun = true)
 
    private val sampleClass = SampleClass(store, snackbar)
 
    @Test
    fun `storeの合計値が+だったらsnackbarが表示されること`() {
        every { store.numbers } returns listOf(1, 2)
 
        sampleClass.calc()
 
        verify { snackbar.show() }
    }
 
    @Test
    fun `storeの合計値が0以下だったらsnackbarが表示されないこと`() {
        every { store.numbers } returns listOf(1, -1)
 
        sampleClass.calc()
 
        verify(exactly = 0) { snackbar.show() }
    }
}

このようにテストを書いて実行速度を計測した結果は以下のようになりました。(1回目はClean Buildの後に実行しています。)

回数 mockito-kotlin MockK
1回目 38ms 515ms
2回目 27ms 533ms
3回目 27ms 539ms

文字通り、実行速度の桁が違いました。mockito-kotlinのほうがおよそ17倍速かったです。

MockKのGithubのこちらのissueでは実行速度について議論されています。

https://github.com/mockk/mockk/issues/13

MockKは起動に時間がかかるが、テストが大きくなればその起動時間も気にならなくなるとも書いてあります。ただ、開発時にはローカルで1つ1つテストを回して確認することも多いので、やはりmockito-kotlinが望ましいという結論になりました。

mockito-kotlinに書き換えて良かったこと

mockito-kotlinに書き換えたことで良かったのは以下の点です。

  • モックの書き方が統一されて、誰がテストを書いてもほとんど同じ形式になった
  • `when` のようなバッククオートを書く必要がなくなったので、見た目もスッキリした
  • any()eq() の拡張関数を自前で用意する必要がなくなった
  • Mockitoからの移行だったので、特に学習コストも移行コストもかからなかった
  • 全てのテストをmockito-kotlinに書き換える過程で、テストのリファクタリングも同時に行えた

特にリファクタリングができたことが大きなメリットでした。 N予備校は今年で5年目になるサービスであり、これまでに様々な開発者が様々な形式でテストを書いていました。 それを一気に書き直すことで、1,000近くのテストの書き方を統一できたのはとても良かったです。

今後もAndroidチームでは、mockito-kotlinをモックの標準ライブラリとして使用していきます。