Open1

ViewModelのテスト

るるすたるるすた

テストの依存関係設定

初期状態では、'testImplementation(libs.junit)'のみ設定されているので
以下2本追加していきます。

libs.versions.toml

バージョン情報とライブラリを登録。

[versions]

kotlinxCoroutinesTest = "1.7.3"
mockk = "1.13.8"

[libraries]

kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinxCoroutinesTest" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }

→Sync Now!!!

build.gradle.kts(Module:app)

いつものdependencies{}の中に追加。

testImplementation(libs.junit)
// コルーチンのテスト用
testImplementation(libs.kotlinx.coroutines.test)
// モックライブラリ
testImplementation(libs.mockk)

→Sync Now!!!

test用ファイルを作成

AndroidStudioにはテスト用のパッケージが2つデフォルトでついてます。

  • test(単体テスト / ユニットテスト)
    場所:src/test/java/...
    特徴:実機やエミュレータ 不要!
    JVM(パソコン上の仮想環境)でサクッとテストできる💨
    ViewModelのロジック、関数の動作など、Androidフレームワークに依存しないものに適している
    速度:早い!

  • androidTest(計測テスト / インストルメンテーションテスト)
    場所:src/androidTest/java/...
    特徴:実機 or エミュレータで実行される📱
    UI操作(Compose画面), Contextの取得, Roomの実データ確認など、
    Androidの機能を使うときに必要
    速度:やや遅め(環境準備が必要)

今回はViewModelのテストなので、ユニットテスト用のファイルを作っていきます。

テストルールの作成

@get:Ruleとは、テストの前後で自動的に特定の準備や後処理をしてくれる仕組み。
以下の場合だと、mainDispatcherRuleをテストを実施するごとに毎回自動的に設定しています。

class DrillViewModelTest {
    @get:Rule
    val mainDispatcherRule = MainDispatcherRule() //後で設定
}

MainDispatcherRuleクラスを作成

DrillViewModelTestクラスの下等にMainDispatcherRuleクラスを作成。
このクラスの役割は、Dispatchers.Main をテスト用の Dispatcher に差し替えること。

  • テストの直前に Dispatchers.Main = testDispatcher に切り替える
  • テストが終わったら resetMain() で元に戻す
@ExperimentalCoroutinesApi
class MainDispatcherRule(
    private val dispatcher: TestDispatcher = StandardTestDispatcher()
):TestWatcher() {
    override fun starting(discription: Description?) {
        Dispatchers.setMain(StandardTestDispatcher())
    }

    override fun finished(description: Description?) {
        Dispatchers.resetMain()
    }
}

テストを作るときに注意すること

非同期処理が含まれている場合

例えば、以下のような非同期処理が含まれているものをテストする場合。。。

    //maxScoreとmaxLevelの取得
    fun loadMaxResult(operator: Operator) {
+        viewModelScope.launch {   //非同期処理
            repository.getMaxResult(operator).collect { result ->
                _uiState.update {
                    it.copy(maxScore = result?.maxScore ?:0,maxLevel = result?.maxLevel ?:0)
                }
            }
        }
    }

非同期処理が終わるまで待ってあげないと、終わる前にテストが始まってしまい、
エラーになることがある。
明示的にこの処理が終わるのを待つコードを追加。
その場合、関数名()=runTest{}としないと、advanceUntilIdel自体がエラーになる。

@Test
fun init_初期化が正しくされていること() = runTest {
    advanceUntilIdle() // 👈 launch{...} が終わるまで待つ

    val state = viewModel.uiState.value

    // モックが呼ばれているか検証(ここでようやくOKになる)
    coVerify(exactly = 1) { repository.getMaxResult(any()) }

    assertNotNull(state.currentQuestion)
    assertNotNull(state.nextQuestion)
}