A ViewModel test fails with Module with the Main dispatcher had failed to initialize
This local JVM test (it lives in src/test) fails before a single assertion runs. The repository is already a fake, and the ViewModel does its work in viewModelScope.
Fix it without moving the test to src/androidTest and without changing the production ViewModel.
class ProfileViewModelTest {
private val viewModel = ProfileViewModel(FakeProfileRepository())
@Test
fun `load fills the state`() = runTest {
viewModel.load()
assertEquals("Ann", viewModel.state.value.name)
}
}
// IllegalStateException: Module with the Main dispatcher had failed to initialize
Find and fix the bug.
viewModelScope runs on Dispatchers.Main, backed by the Android main looper, which does not exist on a local JVM. Install a test dispatcher: Dispatchers.setMain(StandardTestDispatcher()) in @Before and resetMain() in @After.
- ✗Blaming the fake repository or
runTestfor the crash instead of the missing main looper behindDispatchers.Main - ✗Calling
Dispatchers.setMainbut neverresetMain, so the dispatcher leaks into the next test class - ✗Moving a pure-logic ViewModel test to
src/androidTestjust to get a realDispatchers.Main
- →What does
StandardTestDispatcherdo differently fromUnconfinedTestDispatcherhere? - →Why is
Dispatchers.resetMain()in@Afternot optional when the suite has many classes?
The bug
ProfileViewModel does its work in viewModelScope, which dispatches on Dispatchers.Main by default. On Android Dispatchers.Main is backed by the main looper — but in a local JVM test (src/test) the Android framework is stubbed and no looper exists, so the test blows up before the first assertion.
class ProfileViewModelTest {
private val viewModel = ProfileViewModel(FakeProfileRepository()) // ❌ viewModelScope → Dispatchers.Main
// IllegalStateException: Module with the Main dispatcher had failed to initialize
}
The fix
Install a test dispatcher in place of the main one — and always put it back, or the substitution leaks into the next test class.
class ProfileViewModelTest {
private val dispatcher = StandardTestDispatcher()
@Before fun setUp() = Dispatchers.setMain(dispatcher) // ✅ Main → the test scheduler
@After fun tearDown() = Dispatchers.resetMain()
@Test
fun `load fills the state`() = runTest {
val viewModel = ProfileViewModel(FakeProfileRepository())
viewModel.load()
advanceUntilIdle()
assertEquals("Ann", viewModel.state.value.name)
}
}
viewModelScope now runs on runTest's scheduler, so the test controls time itself. In practice the setMain/resetMain pair is extracted into a JUnit rule and reused across every ViewModel test.