Android開発でのMockitoを使ったUnit Testの書き方


この記事では、Android開発のUnit Testで非常によく使われるモックライブラリ、**Mockito** (Mokito-Kotlin)の導入方法と簡単な使い方を解説します

# Mockito
MockitoはJavaのモックライブラリとして現在デファクトスタンダードなライブラリです。
シンプルかつ高機能で、容易に**テストダブル**を作成することができます。

# テストダブル
テストダブルとは、テスト対象が依存しているコンポーネントを置き換える代用品のことです。
（ダブルとはスタントダブル（ = スタントマン）のこと）
テストダブルには、スタブ、モック、スパイ、フェイク、ダミーの5つの役割が定義されており、特に**スタブ**、**モック**、**スパイ**の3つはよく使われます。

|テストダブル|説明|
|:--|:--|
|スタブ（Test Stub）|事前定義した任意の値をテスト対象に与える。|
|モック（Mock Object）|テスト対象メソッド実行時に、テスト対象が依存コンポーネントに与える値や挙動（出力）を検証するために使われる。|
|スパイ（Test Spy）|スタブの上位互換のようなもので、テスト対象に値を与えるのが主な責務。|

# Mockitoの導入
[Mokito](https://site.mockito.org/)はもともとJava向けに作られたライブラリで、一部の機能がKotlinと相性がよくありません。
そのため、Kotlinで扱う場合はMokitoのラッパーライブラリである[Mockito-Kotlin](https://github.com/mockito/mockito-kotlin)を使用するのがおすすめです。
今回はMockito-Kotlinを使った場合の解説をします。

導入は簡単です。使用したいモジュールの`build.gradle`のdependenciesに以下を追記するだけです。

```gradle
testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:<version_code>"
```

`<version_code>`には https://github.com/mockito/mockito-kotlin/tags で最新のバージョンを確認して入れてください

# サンプルクラス
例として、`WeatherForecast`をテスト対象クラスとします。

```kotlin
class WeatherForecast(
    private val satellite: Satellite,
    private val recorder: WeatherRecorder,
    private val formatter: WeatherFormatter
) {
    fun shouldBringUmbrella(latitude: Double, longitude: Double): Boolean =
        when (satellite.getWeather(latitude, longitude)) {
            Weather.RAINY -> true
            Weather.SUNNY, Weather.CLOUDY -> false
        }

    fun recordCurrentWeather(latitude: Double, longitude: Double) {
        val weather = satellite.getWeather(latitude, longitude)
        val description = formatter.format(weather)
        recorder.record(Record(description))
    }
}
```
今回定義されている2つのメソッドのUnit Testを書きたいので、**テスト対象メソッド**は`shouldBringUmbrella()`、`recordCurrentWeather()`とし、
それらの**依存コンポーネント**は`satellite`、`recorder`、`formatter`です。

```kotlin
class Satellite {
    fun getWeather(latitude: Double, longitude: Double): Weather {
        /* 何かしらの処理 */
        return Weather.SUNNY
    }
}

enum class Weather {
    SUNNY, CLOUDY, RAINY
}

class WeatherFormatter {
    fun format(weather: Weather): String = "Weather is $weather"
}

data class Record(val description: String)

class WeatherRecorder {
    fun record(weather: Record) {
        /* 何かしらの処理 */
    }
}
```

# テストコード
先に今回の2つのテスト対象メソッドのテストコードを示します。
Mockitoを使って書くと以下のようになります。

```kotlin
internal class WeatherForecastTest {

    private lateinit var satellite: Satellite   // (1)
    private lateinit var recorder: WeatherRecorder
    private lateinit var formatter: WeatherFormatter
    private lateinit var weatherForecast: WeatherForecast
    private val latitude = 37.58006
    private val longitude = -122.345106

    @BeforeEach
    fun setUp() {
        satellite = mock()
        recorder = mock()
        formatter = spy(WeatherFormatter()) // (3)
        weatherForecast = WeatherForecast(satellite, recorder, formatter)
    }

    // region shouldBringUmbrella
    @Test
    fun `shouldBringUmbrella should return true when weather is RAINY`() {
        doReturn(Weather.RAINY).whenever(satellite).getWeather(any(), any())  // (2)

        val actual = weatherForecast.shouldBringUmbrella(latitude, longitude)
        assertTrue(actual)
    }

    @Test
    fun `shouldBringUmbrella should return false when weather is SUNNY`() {
        doReturn(Weather.SUNNY).whenever(satellite).getWeather(any(), any())

        val actual = weatherForecast.shouldBringUmbrella(latitude, longitude)
        assertFalse(actual)
    }

    @Test
    fun `shouldBringUmbrella should return false when weather is CLOUDY`() {
        doReturn(Weather.CLOUDY).whenever(satellite).getWeather(any(), any())

        val actual = weatherForecast.shouldBringUmbrella(latitude, longitude)
        assertFalse(actual)
    }
    // endregion

    @Test
    fun `recordCurrentWeather should call record`() {
        doReturn(Weather.RAINY).whenever(satellite).getWeather(any(), any())
        weatherForecast.recordCurrentWeather(latitude, longitude)

        argumentCaptor<Record>().apply {    // (4)
            verify(recorder, times(1)).record(capture())
            assertEquals("Weather is RAINY", firstValue.description)
        }

        verify(formatter, times(1)).format(any())
    }
}
```

## shouldBringUmbrella()のテスト

### (1)モックの作成
まず、以下のいずれかの記法で依存コンポーネントのmockを作成します。

```kotlin
val satellite = mock<Satellite>()
```

or

```kotlin
val satellite: Satellite = mock()
```

### (2)スタブメソッドの設定
作成したモックにスタブメソッドを設定します。
`doReturn`で任意の返り値を設定し、wheneverでスタブ化したいメソッドを設定します。
メソッドの引数には、マッチャーを設定することができ、条件を変えてスタブメソッドを作ることができます。

例えば、Matcherを使えば次のような条件でスタブメソッドを作れます。

* 緯度経度が何の場合でもCLOUDYを返す場合、

```kotlin
doReturn(Weather.CLOUDY).whenever(satellite).getWeather(any(), any())
```

* 緯度経度が任意の値でSUNNYを返す場合、

```kotlin
doReturn(Weather.SUNNY).whenever(satellite).getWeather(eq(37.58006), eq(-122.345106))
```

また、doReturenの代わりに、doAnswerを使うとより柔軟に条件を指定することもできます。

* 緯度経度が特定の範囲にあればSUNNYを返す場合

```kotlin
doAnswer {
    val latitude = it.arguments[0] as Double
    val longitude = it.arguments[1] as Double
    if (latitude in 20.424086..45.550999 && longitude in 122.933872..153.980789) {
        return@doAnswer Weather.SUNNY
    } else {
        return@doAnswer Weather.RAINY
    }
}.whenever(satellite).getWeather(any(), any())
```

## recordCurrentWeather()のテスト

### (3)スパイオブジェクトの作成

```kotlin
recorder.record(Record(description))
```
の処理が正しいことをテストするには、

1. record()が呼ばれてること
1. record()の引数には、format()で作成されたdescriptionにより作成されたRecordインスタンスが入っていること

を検証すれば良さそうです。

仮に、formatterのモックオブジェクトを作ったとして、
1は、format()が呼ばれたことを検証するだけなので以下のようにすれば検証できます。

```kotlin
verify(formatter, times(1)).format(any())
```

2は、format()でdescriptionを作成する必要がありますが、モックオブジェクトではformat()が実態のないメソッドとなってしまうため、空文字列が返ってきてしまいます。

そのためには、**スパイオブジェクト**を使います。
スパイオブジェクトは実インスタンスを使うので、**元の実装を残しつつ**、モックと同じような機能を利用することができます。

以下のようにspy()の引数にオブジェクトを指定し、スパイオブジェクトを作成しましょう。

```kotlin
val formatter: WeatherFormatter = spy(WeatherFormatter())
```

### (4)引数の検証

次に、record()の引数を検証します。
**Argument Captor**を使うと、モックのメソッドが呼び出された際の引数をキャプチャしておき、検証することができます。

```kotlin
argumentCaptor<Record>().apply {
    verify(recorder, times(1)).record(capture())
    assertEquals("Weather is RAINY", firstValue.description)
}
```

argumentCaptorの型パラメータにキャプチャするオブジェクトの型を指定し、verifyする際に検証したい引数にcapture()を入れます。
キャプチャしたオブジェクトは`firstValue`に格納されるので、
天気がRAINYのときに、firstValue.descriptionが`"Weather is RAINY`となることを確認すれば完了です。

## 参考
[Androidテスト全書](https://peaks.cc/books/android_testing)