ViewModelのテストを書く
こんにちは、tkyです。
テスト、書いてますか? 僕はぼちぼち書いてます。
今日はViewModelのテストを書いてみたいと思います。
サンプリリポジトリです。サンプルではGitHubのAPIにアクセスする簡単なサンプルにテストを書いてみた感じです。
本稿ではこの2つを扱いたいと思います。
- LiveDataのアサート
- Coroutinesを含むテスト
モックライブラリにはMockKを使っています。mockkの使い方については触れません。
LiveDataのアサート
例えば『View(ActivityやFragment)にデータを渡せているか』をテストしたいときに用います。
val repository = mockk<ProjectRepository>() coEvery { repository.getProjects(any()) } returns emptyList() val observer = mockk<Observer<List<Project>>>(relaxed = true) val viewModel = ProjectListViewModel(repository, testDispatcherProvider) viewModel.projects.observeForever(observer) viewModel.onResume() verify { observer.onChanged(emptyList()) }
ポイントはここ。mockkで作成したObserverをLiveDataに設定します。
val observer = mockk<Observer<List<Project>>>(relaxed = true) viewModel.projects.observeForever(observer)
LiveDataに値が設定されたときにobserver.onChange()が呼ばれることを利用してこのonChange()をアサートします。
verify { observer.onChanged(emptyList()) }
Coroutinesを含むテスト
ほぼCoroutinesを使うケースになるかと思いますが、
- CoroutinesTestRuleを作成
- TestCoroutineScopeでテストを実行
という流れでやっていきます。
@ExperimentalCoroutinesApi class CoroutinesTestRule( val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() ) : TestWatcher() { override fun starting(description: Description?) { super.starting(description) Dispatchers.setMain(testDispatcher) } override fun finished(description: Description?) { super.finished(description) Dispatchers.resetMain() testDispatcher.cleanupTestCoroutines() } }
// このPrivate functionを作っておくと `coroutinesTestRule.testDispatcher.runBlockingTest` を毎回書かなくて良くて便利です private fun test(block: suspend TestCoroutineScope.() -> Unit) = coroutinesTestRule.testDispatcher.runBlockingTest { block() } @Test fun onResume_repositoryのAPIが叩ける() = test { val repository = mockk<ProjectRepository>() coEvery { repository.getProjects(any()) } returns emptyList() val viewModel = ProjectListViewModel(repository, testDispatcherProvider) viewModel.onResume() coVerify(exactly = 1) { repository.getProjects(any()) } }
MockkでCoroutines用のモック(coEvery)とアサーション(coVerify)を使うだけです。 上記の場合『ライフサイクルのonResume()が呼ばれたとき、repository.getProjects()が呼ばれることをアサート』することになります。
ポイントはViewModelのコンストラクタにCoroutinesのDispatcherをDIしていることです。
ViewModelではこの様になっています。
fun onResume() { viewModelScope.launch(dispatcherProvider.io()) { // 普通はviewModelScope.launch(Dispatchers.IO) で良いのだが・・・ try { val response = repository.getProjects("ticktakclock") _projects.value = response } catch (e: Exception) { e.printStackTrace() _projects.value = emptyList() } } }
なぜこのようなことをしているのかというと、 テスト用のコルーチンスコープで動くIOスレッドとViewModelで動くDispatchers.IOのスレッドが異なるため、 テストのcoVerify{}が正しく動作しないことがあるためです。LiveDataも同様に正しく動作しないことがあります。
理由については上記の通りで、図解するとこのようなイメージです。
この図解はAndroid Dev Summit '19にて説明されたもので、詳しくはこちらのアーカイブを見ていただいたらよいかと思います。 www.youtube.com
まとめ
- livedataとCoroutinesのテストがかけるようになった
- Coroutinesのテストのテストを見越してDispatcherもDIしておくと後々楽になるかも