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)
}