Home > front end >  kotlinx.coroutines.test the latest API usage
kotlinx.coroutines.test the latest API usage

Time:01-04

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

  • Related