LiveData の Unit Test（UnitTest探求記2）

Unit Test 探求記は、unit test に関してまったりと実験しつつその過程を綴ってみるというものです。

# 今回のお題
LiveData の unit test を行ってみます。

# テスト対象
今回は、[Transformations#map(LiveData, Function)](https://developer.android.com/reference/kotlin/androidx/lifecycle/Transformations#map(androidx.lifecycle.LiveData,%20androidx.arch.core.util.Function)) に対して複数の [LiveData](https://developer.android.com/reference/kotlin/androidx/lifecycle/LiveData) を受けられるようにしたメソッドを作成して、それをテスト対象とします。

## ◆ TransformationsUtils
[TransformationsUtils.kt](https://github.com/beyondseeker/of-commons/blob/90b55912d5f8fbc377d97c7ca9d8905bc1fe5a4f/android/src/main/java/com/objectfanatics/commons/androidx/lifecycle/TransformationsUtils.kt)

```kotlin:TransformationsUtils.kt
package com.objectfanatics.commons.androidx.lifecycle

import androidx.annotation.MainThread
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData

object TransformationsUtils {
    /**
     * @see androidx.lifecycle.Transformations.map
     */
    @MainThread
    fun <T> map(vararg sources: LiveData<out Any?>, function: () -> T): LiveData<T> =
        MediatorLiveData<T>().apply {
            sources.forEach { source ->
                addSource(source) {
                    value = function()
                }
            }
        }
}
```

# mockk への依存関係追加

## ◆ build.gradle.kts
[mockk](https://mockk.io/) への依存関係を追加します。

```kotlin:build.gradle.ktsへの追加例
dependencies {
    // for mockk
    // @see https://mockk.io/#installation
    testImplementation("io.mockk:mockk:1.10.0")
}
```

# テスト1
まずはシンプルなテストを作成してみます。

## ◆ TransformationsUtilsTest（失敗）
[source tree](https://github.com/beyondseeker/of-commons/tree/bde8b4eb51796c1913cafc1ceb28e393fe9f9d04)
[TransformationsUtilsTest.kt](https://github.com/beyondseeker/of-commons/blob/bde8b4eb51796c1913cafc1ceb28e393fe9f9d04/android/src/test/java/com/objectfanatics/commons/androidx/lifecycle/TransformationsUtilsTest.kt)

```kotlin:TransformationsUtilsTest.kt
package com.objectfanatics.commons.androidx.lifecycle

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import io.mockk.confirmVerified
import io.mockk.mockk
import io.mockk.verify
import org.junit.Test

class TransformationsUtilsTest {
    interface Recorder<T> {
        fun record(value: T)
    }

    /**
     * 3つの[MutableLiveData]を[TransformationsUtils]で監視するテスト。
     */
    @Test
    fun map_forQiita1() {
        // テストの実行経過を記録する Recorder の用意
        val recorder = mockk<Recorder<String>>(relaxed = true)

        // 3 つの MutableLiveData を用意
        val input1: MutableLiveData<String?> = MutableLiveData()
        val input2: MutableLiveData<String?> = MutableLiveData()
        val input3: MutableLiveData<String?> = MutableLiveData()

        // 上記 3 つの MutableLiveData を受けて文字列を返す LiveData を用意
        val output: LiveData<String> = TransformationsUtils.map(input1, input2, input3) {
            "${input1.value}, ${input2.value}, ${input3.value}"
        }

        // 実行結果が標準出力に表示されるように準備
        output.observeForever { value ->
            recorder.record(value)
        }

        // テストの実行
        input1.postValue("red")
        input2.postValue("blue")
        input3.postValue("green")

        // 実行内容の検証
        verify {
            recorder.record("red, null, null")
            recorder.record("red, blue, null")
            recorder.record("red, blue, green")
        }

        // 実行内容の検証がすべて完了したことの確認
        confirmVerified(recorder)
    }
}
```

これを実行してみると、以下のようなエラーになりました。

```
$ ./gradlew test --info
com.objectfanatics.commons.androidx.lifecycle.TransformationsUtilsTest > map_forQiita1 FAILED
    java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
        at android.os.Looper.getMainLooper(Looper.java)
        at androidx.arch.core.executor.DefaultTaskExecutor.isMainThread(DefaultTaskExecutor.java:77)
        at androidx.arch.core.executor.ArchTaskExecutor.isMainThread(ArchTaskExecutor.java:116)
        at androidx.lifecycle.LiveData.assertMainThread(LiveData.java:486)
        at androidx.lifecycle.LiveData.observeForever(LiveData.java:224)
        at com.objectfanatics.commons.androidx.lifecycle.TransformationsUtilsTest.map_forQiita1(TransformationsUtilsTest.kt:34)
```

## ◆ Test helpers for LiveData の導入
`android.os.Looper#getMainLooper()` の値は Android の実行環境が `Looper` にセットするものなので、unit test 環境では利用できません。そこで、以下の dependency を追加します。

```kotlin:build.gradle.ktsへの追加例
dependencies {
    // @see https://developer.android.com/jetpack/androidx/releases/lifecycle
    testImplementation("androidx.arch.core:core-testing:2.1.0")
}
```

## ◆ TransformationsUtilsTest（成功）
[source tree](https://github.com/beyondseeker/of-commons/tree/b8f4d9eb189f79af388769a024c13c8cc89bb673)
[TransformationsUtilsTest.kt](https://github.com/beyondseeker/of-commons/blob/b8f4d9eb189f79af388769a024c13c8cc89bb673/android/src/test/java/com/objectfanatics/commons/androidx/lifecycle/TransformationsUtilsTest.kt)

下記のように Rule を指定することにより、ユニットテストが正常動作するようになります。

```kotlin:TransformationsUtilsTest.kt差分
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import org.junit.Rule
import org.junit.rules.TestRule

class TransformationsUtilsTest {
    @Rule
    @JvmField
    val rule: TestRule = InstantTaskExecutorRule()
}
```

※ `@JvmField` を忘れると `org.junit.internal.runners.rules.ValidationError: The @Rule 'rule' must be public.` と怒られます。

# テスト2
red, blue, green のテストは内容的に味気ないので、追加でテストを作成してみます。[^1]

[source tree](https://github.com/beyondseeker/of-commons/tree/eb6f065929efbd2e7b0531aa7fd4209a6c3c054a)

- 場面設定
    - 名前と苗字を入力するための入力フォームがある。
    - 送信ボタンは、名前と苗字の両方が空でない場合のみ押すことができる。
- 仕様
    - 名前入力用の MutableLiveData<String> がある。
    - 苗字入力用の MutableLiveData<String> がある。
    - 送信ボタンの押下可否を表す LiveData<Boolean> を TransformationsUtils.map() によって作成する。
- シナリオ
    - 最初は、名前も苗字も null.
    - 名前を入力する。苗字が null なので送信ボタンは押せない。
    - 苗字も入力する。名前も苗字も空でないので、送信ボタンは押せる。

## ◆ TransformationsUtilsTest
結果が表示されるのではなく、結果を検証するようにコードを変更します。
[TransformationsUtilsTest.kt](https://github.com/beyondseeker/of-commons/blob/eb6f065929efbd2e7b0531aa7fd4209a6c3c054a/android/src/test/java/com/objectfanatics/commons/androidx/lifecycle/TransformationsUtilsTest.kt)
[diff](https://github.com/beyondseeker/of-commons/commit/eb6f065929efbd2e7b0531aa7fd4209a6c3c054a)

```kotlin:TransformationsUtilsTest.kt抜粋
/**
 * フォーム(名前、苗字)の両方が空でない時にのみ送信ボタンが押せるという仕様を想定した内容のテスト。
 * ※ Unit test としては微妙だが、qiita 記事的にわかりやすいものにした。
 */
@Test
fun map_forQiita2() {
    // テストの実行経過を記録する Recorder の用意
    val recorder = mockk<TransformationsUtilsTest.Recorder<Boolean>>(relaxed = true)

    // 名前、苗字の MutableLiveData を用意
    val firstName: MutableLiveData<String?> = MutableLiveData()
    val lastName: MutableLiveData<String?> = MutableLiveData()
    val forms = listOf(firstName, lastName)

    // 送信ボタンが押せるかどうかの MutableLiveData を用意。
    // ※名前、苗字の両方が空でない場合のみ true になる
    val isSubmitButtonEnabled: LiveData<Boolean> =
        TransformationsUtils.map(firstName, lastName) {
            !forms.any { it.value.isNullOrEmpty() }
        }

    // 実行経過が Recorder に記録されるように準備
    isSubmitButtonEnabled.observeForever { value ->
        recorder.record(value)
    }

    // テストの実行
    firstName.postValue("シャミ子")
    lastName.postValue("吉田")

    // 実行内容の検証
    verify {
        // firstName が "シャミ子" になったが lastName が null のため false
        recorder.record(false)
        // firstName が "シャミ子"、lastName が "吉田" になったため true
        recorder.record(true)
    }

    // 実行内容の検証がすべて完了したことの確認
    confirmVerified(recorder)
}

```

# テスト結果
下記のように、テストに成功しました。
![test_result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/219474/c9ff4945-07a7-5414-18af-bdb512239510.png)


# まとめ
今回は、[mockk](https://mockk.io/) を使いつつ、Rule を指定して LiveData の unit test を行いました。

[^1]: 本来はこのようなシナリオにするのは良くないのですが、わかりやすさのためにあえてやりました。後悔はしてない。