Unit Testing ViewModels
ViewModels are an ideal unit-testing target precisely because they're designed to hold no reference to the Android UI framework — no Context, no View, no Composable — and instead expose state through plain Kotlin types like StateFlow. That means a ViewModel test can run entirely on the JVM using a tool like JUnit and Robolectric-free plain unit tests, without an emulator or device, executing in milliseconds. The main obstacle is that ViewModels typically launch coroutines through viewModelScope, which is bound to Dispatchers.Main by default, and Dispatchers.Main is not available in a plain JVM unit test environment — attempting to use it without configuration throws an exception. The standard fix is to replace the main dispatcher with a test dispatcher for the duration of each test.
Cricket analogy: Just as a batting coach can analyze technique from video footage alone without a live stadium, a ViewModel test runs on the plain JVM without Context or an emulator, though viewModelScope's default Dispatchers.Main needs swapping for tests since it's unavailable off-device.
Replacing Dispatchers.Main with a test dispatcher
kotlinx-coroutines-test provides Dispatchers.setMain(testDispatcher) to redirect viewModelScope's coroutines onto a controllable dispatcher for the duration of a test, and Dispatchers.resetMain() to restore normal behavior afterward — typically wired up via a JUnit @Rule or, in JUnit5/newer setups, in @Before/@After functions. Using StandardTestDispatcher (rather than immediately-executing UnconfinedTestDispatcher) gives you explicit control: coroutines launched inside the ViewModel don't run until you call advanceUntilIdle() or runTest's automatic virtual-time advancement, which lets you assert on intermediate states like 'loading' before the coroutine completes and moves to 'success' or 'error'.
Cricket analogy: Using StandardTestDispatcher is like reviewing a match ball-by-ball on demand instead of watching it live in real time, letting you pause after 'rain delay' state and assert on it before calling advanceUntilIdle() to resume to the final result.
Faking dependencies and asserting on StateFlow
Because the ViewModel depends on a repository interface rather than a concrete Room/Retrofit implementation, tests supply a fake or a mocking-framework mock that returns predetermined values or throws predetermined errors, letting you exercise both the success and failure paths of the ViewModel's logic without any real I/O. To assert on a StateFlow's emitted values, the most robust approach is Turbine, a small testing library that lets you collect a Flow inside a test and assert on the sequence of emitted values with awaitItem(), rather than manually collecting into a list and hoping timing lines up.
Cricket analogy: Just as a coach can run tactical drills against a practice bowling machine instead of a real bowler to test both a successful cover drive and a missed swing, a ViewModel test uses a fake repository to exercise both success and failure paths without real I/O.
@OptIn(ExperimentalCoroutinesApi::class)
class TaskListViewModelTest {
private val testDispatcher = StandardTestDispatcher()
@Before
fun setUp() {
Dispatchers.setMain(testDispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `loading tasks emits Loading then Success`() = runTest {
val fakeRepository = FakeTaskRepository(
tasks = listOf(Task(id = 1, title = "Buy milk"))
)
val viewModel = TaskListViewModel(fakeRepository)
viewModel.uiState.test {
assertEquals(TaskUiState.Loading, awaitItem())
advanceUntilIdle()
val success = awaitItem() as TaskUiState.Success
assertEquals(1, success.tasks.size)
assertEquals("Buy milk", success.tasks.first().title)
}
}
@Test
fun `repository failure emits Error state`() = runTest {
val fakeRepository = FakeTaskRepository(shouldFail = true)
val viewModel = TaskListViewModel(fakeRepository)
viewModel.uiState.test {
skipItems(1) // Loading
advanceUntilIdle()
assertTrue(awaitItem() is TaskUiState.Error)
}
}
}runTest, StandardTestDispatcher, and virtual time together mean a test that would take real seconds (e.g. delay(2000)) executes instantly, because the test dispatcher advances a simulated clock instead of actually waiting.
Forgetting Dispatchers.setMain()/resetMain() is one of the most common ViewModel-testing failures — tests either crash with 'Module with the Main dispatcher had failed to initialize' or, worse, pass inconsistently depending on execution order across a test suite because state from a previous test's dispatcher leaks in.
- ViewModels are easy to unit test because they hold no Android framework references and expose state via StateFlow.
- Dispatchers.setMain(testDispatcher) / resetMain() is required because viewModelScope defaults to Dispatchers.Main, unavailable on the JVM.
- StandardTestDispatcher gives explicit control over when launched coroutines actually execute, via advanceUntilIdle().
- Fake or mocked repository implementations let tests exercise both success and failure paths without real I/O.
- Turbine's awaitItem()/skipItems() give reliable, ordered assertions over StateFlow emissions.
- runTest uses virtual time, so even delay()-based logic in the ViewModel executes instantly in tests.
Practice what you learned
1. Why is Dispatchers.setMain(testDispatcher) necessary when unit testing a ViewModel?
2. What advantage does StandardTestDispatcher provide over UnconfinedTestDispatcher for ViewModel tests?
3. What is Turbine primarily used for in ViewModel tests?
4. Why do ViewModel unit tests typically inject a fake repository rather than a real Room/Retrofit-backed one?
5. What effect does runTest's virtual time have on a suspend function containing delay(2000)?
Was this page helpful?
You May Also Like
ViewModel Basics
What Android's ViewModel class does, how it survives configuration changes, and how to create and scope ViewModels correctly in a Compose app.
Repository Pattern in Android
The repository pattern centralizes data access behind a single abstraction, hiding whether data comes from Room, Retrofit, or DataStore from ViewModels and the UI layer.
Coroutines Basics in Android
Kotlin coroutines provide a lightweight, structured way to write asynchronous, non-blocking code on Android using suspend functions, coroutine builders, dispatchers, and scopes.
Compose UI Testing
Compose UI tests drive composables through a semantics tree using finders, actions, and assertions, running on an instrumented device or Robolectric with a synchronized test clock.