2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

LiveData の Unit Test(UnitTest探求記2)

Last updated at Posted at 2020-08-12

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

今回のお題

LiveData の unit test を行ってみます。

テスト対象

今回は、Transformations#map(LiveData, Function) に対して複数の LiveData を受けられるようにしたメソッドを作成して、それをテスト対象とします。

◆ TransformationsUtils

TransformationsUtils.kt

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 への依存関係を追加します。

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

テスト1

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

◆ TransformationsUtilsTest(失敗)

source tree
TransformationsUtilsTest.kt

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 を追加します。

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
TransformationsUtilsTest.kt

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

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

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

◆ TransformationsUtilsTest

結果が表示されるのではなく、結果を検証するようにコードを変更します。
TransformationsUtilsTest.kt
diff

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

まとめ

今回は、mockk を使いつつ、Rule を指定して LiveData の unit test を行いました。

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

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?