【Android】ViewModel + LiveData のUNITテストを書く


はじめに

Android では MVVM のアーキテクチャでの実装が一般的になっています。
ViewModel と LiveData を組み合わせた実装が公式サイトでも紹介されているのでリンクを貼っておきます。

(公式)アプリ アーキテクチャ ガイド

今回は
ViewModel + LiveData
の実装における UNITテストを紹介していこうと思います。

ViewModelの実装

まずはテスト対象のクラスを用意したいと思います。
APIにリクエストしてレスポンスを返すサンプルアプリを想定して実装することにします。
せっかくなので上記の公式サイトの例を参考にして
ViewModel、Respository、DataSource のクラスをそれぞれ用意することにしました。

まず、テスト対象の ViewModel クラスを実装します。
リクエスト用のメソッドと LiveData だのシンプルな実装にしました。
リクエストした結果の SampleRequest オブジェクトを View に返す実装です。

SampleViewModel.kt
class SampleViewModel(
    private val repository: SampleRequestRespository
): ViewModel() {

    private val _sampleRequest = MutableLiveData<SampleRequest>()
    val sampleRequest: LiveData<SampleRequest> = _sampleRequest

    // Activity や Fragment からリクエストするメソッド
    fun loadSampleRequest() {
        viewModelScope.launch {
            _sampleRequest.value = repository.getSampleRequest()
        }
    }
}

その他必要なクラスも用意します。
APIへ本当にリクエストする処理は今回は省略しています。

SampleRequestRespository.kt
class SampleRequestRespository(
    private val remoteSource: SampleRemoteDataSource
) {
    suspend fun getSampleRequest(): SampleRequest = remoteSource.getSampleRequest()
}
SampleRemoteDataSource.kt
class SampleRemoteDataSource {
    fun getSampleRequest(): SampleRequest = SampleRequest()
}
SampleRequest.kt
// テスト用なので適当に定義
class SampleRequest {
    val sampleRequestItem1: String? = null
    val sampleRequestItem2: Int? = null
}

これでテスト対象のクラス実装は完了です。

ViewModelのテスト

mockk を利用したテストを書きます。
gradle ファイルに以下の定義を追加します。

build.gradle
dependencies {
    
    
    
    // ↓を追加(最新のバージョンにしてください)
    testImplementation 'io.mockk:mockk:1.10.5'
    testImplementation 'androidx.arch.core:core-testing:2.1.0'
    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.1.0'
}

mockk を利用するので mockk はわかると思うのですが、その他はテスト実行時の Exception で失敗にならないようにする為の定義です。

androidx.arch.core:core-testing は

java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.

対策で
org.jetbrains.kotlinx:kotlinx-coroutines-test は

Exception in thread "main" java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used

対策です。

gradle の定義が追記できたのでテストを書いていきます。

SampleViewModelTest.kt
// Exception in thread "main" java.lang.IllegalStateException 対策で必要
@ExperimentalCoroutinesApi
class SampleViewModelTest {

    // java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. 対策で必要
    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()


    @Before
    fun setUp() {
        // Exception in thread "main" java.lang.IllegalStateException 対策で必要
        Dispatchers.setMain(Dispatchers.Unconfined)
    }

    @After
    fun tearDown() {
        // Exception in thread "main" java.lang.IllegalStateException 対策で必要
        Dispatchers.resetMain()
    }

    @Test
    fun loadSampleRequest_正常系テスト() {
        val mockRepository = mockk<SampleRequestRespository>()
        val target = SampleViewModel(mockRepository)
        val result = SampleRequest()

        // coEvery を使用することで suspend fun の mockRepository.getSampleRequest() の戻り値を明示的に返せる
        coEvery { mockRepository.getSampleRequest() } returns result

        // ViewModel の sampleRequest が変更されたことを確認する為 observer を mock する
        val mockObserver = spyk<Observer<SampleRequest>>()
        target.sampleRequest.observeForever(mockObserver)

        // テスト対象のメソッドを呼び出す
        target.loadSampleRequest()

        // mockObserver が mockRepository.getSampleRequest() の結果で呼ばれたことを確認する
        verify(exactly = 1) {
            mockObserver.onChanged(result)
        }
    }
}

これでテストを実行すると無事成功しました。

今回は正常系のテストだけでしたが、推奨されるアーキテクチャを使うことでテストも書きやすい設計にもなるかと思います。