20210301のAndroidに関する記事は1件です。

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

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

Mockito

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

テストダブル

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

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

Mockitoの導入

MokitoはもともとJava向けに作られたライブラリで、一部の機能がKotlinと相性がよくありません。
そのため、Kotlinで扱う場合はMokitoのラッパーライブラリであるMockito-Kotlinを使用するのがおすすめです。
今回はMockito-Kotlinを使った場合の解説をします。

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

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

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

サンプルクラス

例として、WeatherForecastをテスト対象クラスとします。

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()とし、
それらの依存コンポーネントsatelliterecorderformatterです。

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を使って書くと以下のようになります。

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を作成します。

val satellite = mock<Satellite>()

or

val satellite: Satellite = mock()

(2)スタブメソッドの設定

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

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

  • 緯度経度が何の場合でもCLOUDYを返す場合、
doReturn(Weather.CLOUDY).whenever(satellite).getWeather(any(), any())
  • 緯度経度が任意の値でSUNNYを返す場合、
doReturn(Weather.SUNNY).whenever(satellite).getWeather(eq(37.58006), eq(-122.345106))

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

  • 緯度経度が特定の範囲にあればSUNNYを返す場合
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)スパイオブジェクトの作成

recorder.record(Record(description))

の処理が正しいことをテストするには、

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

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

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

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

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

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

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

val formatter: WeatherFormatter = spy(WeatherFormatter())

(4)引数の検証

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

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テスト全書

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む