Kotlin coroutinesを用いたTestで、処理が思うような順番で実行されない時の対処法


はじめに

ViewModelでよくあるようなパターンの、suspend functionではなく内部でコルーチンを起動しているようなメソッドを書くときに詰まったのでメモ用に投稿します。


class SampleViewModel(
    private val sampleApi: SampleApi
) : ViewModel() {
    private val _sampleStateFlow = MutableStateFlow(false)
    val sampleStateFlow: StateFlow<Boolean>() 
        get() = _sampleStateFlow

    fun sample() {
        viewModelScope.launch {
             _sampleStateFlow.value = true
        }
    }
}

こういうメソッドではメソッドの内部でコルーチンを起動している関係上、sample()内部の処理が完全に終わった後にその後の処理が実行されるわけではないので


   @Test
   fun sample_test() {
      viewModel.sample()
      assertThat(viewModel.sampleStateFlow.value).isTrue() //assertionにgoogle/truthを使用
   }

のように書くとテストが失敗する場合があります。今までは


   @Test
   fun sample_test() = runBlockingTest {
      viewModel.sample()
      assertThat(viewModel.sampleStateFlow.value).isTrue()
   }

とすれば良かったのですが、runBlockingTestが非推奨になっているのでその代替策をまとめます。

runTestとUnconfinedTestDispatcher()を併用する

   @Before
   fun setUp() {
      Dispatchers.setMain(UnconfinedTestDispatcher())
   }

   @After
   fun after() {
      Dispatchers.resetMain()
   }
   
   @Test
   fun sample_test() = runTest() {
      viewModel.sample()
      assertThat(viewModel.sampleStateFlow.value).isTrue()
   }

advanceUntilIdle()を用いる


   @Test
   fun sample_test() = runTest() {
      viewModel.sample()
      advanceUntilIdle() //testScopeの中で待機していたコルーチンの処理が終わるまで待ってくれる

      assertThat(viewModel.sampleStateFlow.value).isTrue()
   }

Job型で返して、テストメソッドでjoin()する

//SampleViewModel側
fun sample() = viewModelScope.launch {
    _sampleStateFlow.value = true
}

@Test
fun sample_test() = runTest {
  viewModel.sample().join()
  
  assertThat(viewModel.sampleStateFlow.value).isTrue() //assertionにgoogle/truthを使用
}

参考元リンク

終わりに

公式の文献をちゃんと読むことが大事だと再認識しました…。

参考文献