Search code examples
androidkotlinunit-testingjunitkotlin-coroutines

Testing Dispatcher IO and ViewModelScope in Unit Tests


I'm trying to test some code in my viewmodel. I'm trying to make two network calls with dispatcher async, but haven't figured out how to test. This is a simplified example of what I'm essentially trying to do,

// View Model Function

fun initialise() {
    viewModelScope.launch(coroutineExceptionHandler) {
        val accountDeferred = async(Dispatchers.IO) { accountModel.getAccounts() }
        val contentDeferred = async(Dispatchers.IO) { contentModel.getContent() }
        val account = accountDeferred.await()
        val content = contentDeferred.await()

        handleResult(account, content) // viewState is updated
    }
}

// Unit Test
// Simplified

class Test {
    @get:Rule
    val coroutineRule = CoroutineTestRule(StandardTestDispatcher())

    @Test
    fun `This is the test`() {
        runTest{
            whenever(accountModel.getAccounts()).thenReturn(
                Result.success( getAccountContent() , ReasonStatus.empty()))
            whenever(contentModel.getContent()).thenReturn(
                Result.success( getContent(), ReasonStatus.empty()))

            // start view model
            viewmodel = ViewModel(accountModel, contentModel)
            runCurrent()

            // In debugging mode, the variable viewState is set, before the two jobs in the viewmodel finish

            val viewState = viewmodel.viewState.value

            // check contents
            assertNotEmpty(viewState.accounts)

            // always fails since the network calls have not completed
        }
    }
}

What would be some suggestions to ensure that the two async awaits execute before the other code in runTest block executes first in a reliable manner?


Solution

  • You need to use dependency injection to provide IO dispatcher to your ViewModel to make it testable. Then you can use the injected dispatcher instead of hard coding Dispatchers.IO:

    class ViewModel(
        //...
        private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
    ) {
    //...
    fun initialise() {
        viewModelScope.launch(coroutineExceptionHandler) {
            val accountDeferred = async(ioDispatcher) { accountModel.getAccounts() }
            val contentDeferred = async(ioDispatcher) { contentModel.getContent() }
    
    

    From the documentation on testing coroutines:

    If the Main dispatcher has been replaced with a TestDispatcher, any newly-created TestDispatchers will automatically use the scheduler from the Main dispatcher, including the StandardTestDispatcher created by runTest if no other dispatcher is passed to it.

    This makes it easier to ensure that there is only a single scheduler in use during the test. For this to work, make sure to create all other TestDispatcher instances after calling Dispatchers.setMain.

    Assuming that CoroutineTestRule sets the Main dispatcher, you can now create a new TestDispatcher to inject and it will use the same scheduler:

    @OptIn(ExperimentalCoroutinesApi::class)
    @Test
    fun `This is the test`() = runTest {
        val dispatcher = StandardTestDispatcher()
        val viewmodel = ViewModel(/*other args*/, dispatcher)
        advanceUntilIdle()
        //...
    }