Тест ViewModel падает с Module with the Main dispatcher had failed to initialize
Этот локальный JVM-тест (он лежит в src/test) падает ещё до первой проверки. Repository уже подменён на fake, а ViewModel делает свою работу в viewModelScope.
Почините тест, не перенося его в src/androidTest и не меняя боевой 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
Найдите и исправьте ошибку.
viewModelScope работает на Dispatchers.Main, за которым стоит главный looper Android, а его на локальной JVM нет. Подставьте тестовый диспетчер: Dispatchers.setMain(StandardTestDispatcher()) в @Before и Dispatchers.resetMain() в @After.
- ✗Винить в падении fake-repository или
runTestвместо отсутствующего главного looper заDispatchers.Main - ✗Вызывать
Dispatchers.setMain, но неresetMain, из-за чего диспетчер утекает в следующий тестовый класс - ✗Переносить чисто логический тест ViewModel в
src/androidTestтолько ради настоящегоDispatchers.Main
- →Чем
StandardTestDispatcherотличается здесь отUnconfinedTestDispatcher? - →Почему
Dispatchers.resetMain()в@Afterне является необязательным, когда классов в наборе много?
Баг
ProfileViewModel запускает работу в viewModelScope, а тот по умолчанию выполняется на Dispatchers.Main. На Android за Dispatchers.Main стоит главный looper, но в локальном JVM-тесте (src/test) фреймворк Android заглушен и looper-а нет — поэтому падение происходит ещё до первой проверки.
class ProfileViewModelTest {
private val viewModel = ProfileViewModel(FakeProfileRepository()) // ❌ viewModelScope → Dispatchers.Main
// IllegalStateException: Module with the Main dispatcher had failed to initialize
}
Исправление
Подставьте вместо главного диспетчера тестовый — и обязательно верните всё назад, иначе подмена утечёт в следующий тестовый класс.
class ProfileViewModelTest {
private val dispatcher = StandardTestDispatcher()
@Before fun setUp() = Dispatchers.setMain(dispatcher) // ✅ Main → тестовый планировщик
@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 работает на планировщике runTest, поэтому тест управляет временем сам. Ту же пару setMain/resetMain обычно выносят в правило JUnit и переиспользуют во всех тестах ViewModel.