I am working on a test case for ViewModel
classes with the recent coroutines-test API and it doesn't work as expected.
@Test
fun `when balanceOf() is called with existing parameter model state is updated with correct value`() = runTest {
Dispatchers.setMain(StandardTestDispatcher())
fakeWalletRepository.setPositiveBalanceOfResponse()
assertThat("Model balance is not default", subj.uiState.value.wallet.getBalance().toInt() == 0)
assertThat("Errors queue is not empty", subj.uiState.value.errors.isEmpty())
assertThat("State is not default", subj.uiState.value.status == Status.NONE)
subj.balanceOf("0x6f1d841afce211dAead45e6109895c20f8ee92f0")
advanceUntilIdle()
assertThat("Model balance is not updated with correct value", subj.uiState.value.wallet.getBalance().toLong() == 42L)
assertThat("Errors queue is not empty", subj.uiState.value.errors.isEmpty())
assertThat("State is not set as BALANCE", subj.uiState.value.status == Status.BALANCE)
}
he issue is not working stably - usually it fails, under debugger usually it passes.
Based on my understanding StandartTestDispatcher
shouldn't run coroutines until advanceUntilIdle
call when UnconfinedTestDispatcher
run them immediately. advanceUntilIdle
should wait until all coroutines are finished, but it seems there is a race condition in the next assertThat()
call which causes ambiguity in the behaviour of my test case.
advanceUntilIdle
should guarantee all coroutines end their work. Does it mean race condition occurs somewhere under .collect{}
or state.update {}
calls? (In my understanding advanceUntilIdle
should wait end of their execution too)
fun balanceOf(owner: String) {
logger.d("[start] balanceOf()")
viewModelScope.launch {
repository.balanceOf(owner)
.flowOn(Dispatchers.IO)
.collect { value ->
logger.d("collect get balance result")
processBalanceOfResponse(value)
}
}
logger.d("[end] balanceOf()")
}
state.update {
it.wallet.setBalance(value.data)
it.copy(wallet = it.wallet, status = Status.BALANCE)
}
CodePudding user response:
From what i see the balanceOf() is executed on the IO dispatcher and you collect in the viewModelScope (which is the Main.Immediate dispatcher). The IO dispatcher is not overridden in your test and this is what causes the unpredictability of your test. As there’s currently no way to override the IO dispatcher like with setMain, you can add the ability to override the background dispatcher in your ViewModel by adding a default argument, for example :
ViewModel(private val backgroundDispatcher: CoroutineDispatcher = Dispatchers.IO)
And replace it in your code :
fun balanceOf(owner: String) {
logger.d("[start] balanceOf()")
viewModelScope.launch {
repository.balanceOf(owner)
.flowOn(backgroundDispatcher)
.collect { value ->
logger.d("collect get balance result")
processBalanceOfResponse(value)
}
}
logger.d("[end] balanceOf()")
}
Then in your test you instantiate the ViewModel with the standard test dispatcher and it should work. You can check this page to understand the issue : https://developer.android.com/kotlin/coroutines/test#injecting-test-dispatchers