Unit Test 探求記は、unit test に関してまったりと実験しつつその過程を綴ってみるというものです。
今回のお題
LiveData の unit test を行ってみます。
テスト対象
今回は、Transformations#map(LiveData, Function) に対して複数の LiveData を受けられるようにしたメソッドを作成して、それをテスト対象とします。
◆ TransformationsUtils
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 への依存関係を追加します。
dependencies {
// for mockk
// @see https://mockk.io/#installation
testImplementation("io.mockk:mockk:1.10.0")
}
テスト1
まずはシンプルなテストを作成してみます。
◆ TransformationsUtilsTest(失敗)
source tree
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 を追加します。
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 を指定することにより、ユニットテストが正常動作するようになります。
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
- 場面設定
- 名前と苗字を入力するための入力フォームがある。
- 送信ボタンは、名前と苗字の両方が空でない場合のみ押すことができる。
- 仕様
- 名前入力用の MutableLiveData がある。
- 苗字入力用の MutableLiveData がある。
- 送信ボタンの押下可否を表す LiveData を TransformationsUtils.map() によって作成する。
- シナリオ
- 最初は、名前も苗字も null.
- 名前を入力する。苗字が null なので送信ボタンは押せない。
- 苗字も入力する。名前も苗字も空でないので、送信ボタンは押せる。
◆ TransformationsUtilsTest
結果が表示されるのではなく、結果を検証するようにコードを変更します。
TransformationsUtilsTest.kt
diff
/**
* フォーム(名前、苗字)の両方が空でない時にのみ送信ボタンが押せるという仕様を想定した内容のテスト。
* ※ 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)
}
テスト結果
まとめ
今回は、mockk を使いつつ、Rule を指定して LiveData の unit test を行いました。
-
本来はこのようなシナリオにするのは良くないのですが、わかりやすさのためにあえてやりました。後悔はしてない。 ↩