Skip to content
AB

Testing Coroutine ViewModels with Controlled Dispatchers

Drive intents, virtual time, flows, cancellation, and repository outcomes deterministically.

Ashish BhakhandPublished

Tests using real delays are slow and flaky. Coroutines test utilities provide a scheduler and virtual time.

Code
@Test
fun refresh_exposes_data() = runTest {
    val repository = FakeRepository(result = listOf(Report.fixture()))
    val viewModel = ReportsViewModel(repository, StandardTestDispatcher(testScheduler))

    viewModel.onIntent(ReportsIntent.Refresh)
    advanceUntilIdle()

    assertEquals(1, viewModel.state.value.reports.size)
    assertFalse(viewModel.state.value.loading)
}

Inject dispatchers instead of hardcoding Dispatchers.IO in business classes. Production supplies real dispatchers; tests share the scheduler.

Collect side-effect flows before sending the intent so events are not missed. Turbine or a small collector can assert their sequence.

Test cancellation explicitly: begin a suspended request, send a replacement intent, complete the old request, and prove stale data does not win.

Fakes often communicate behavior more clearly than mocks for repositories and streams. Record calls only when the interaction itself is part of the contract.

The test should describe user-observable state transitions: loading, success, error, retry, and cancellation. Virtual time makes asynchronous behavior precise without making the implementation synchronous.

← Back to all writing