Kotlin协程ViewModel单元测试失效,主调度器初始化失败
ViewModel单元测试报错:主调度器模块初始化失败
问题背景
原ViewModel单元测试方案失效,为使viewModelScope.launch创建的协程正常运行,已按照官方文档实现MainDispatcherRule重置主调度器为测试调度器,但测试编译通过后运行失败,报错主调度器模块初始化失败。
相关代码
MainDispatcherRule实现
@ExperimentalCoroutinesApi class MainDispatcherRule( val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : TestWatcher() { override fun starting(description: Description) { Dispatchers.setMain(testDispatcher) } override fun finished(description: Description) { Dispatchers.resetMain() } }
RestaurantsViewModel代码
@HiltViewModel class RestaurantsViewModel @Inject constructor(private val repo: RestaurantsRepository): ViewModel() { private val _restaurantsState = MutableLiveData<ResultState<List<Restaurant>>>() val restaurantsState: LiveData<ResultState<List<Restaurant>>> = _restaurantsState init { initialize() } fun initialize() { viewModelScope.launch { _restaurantsState.postValue(ResultState.Loading) try { _restaurantsState.postValue( ResultState.Success(repo.getRestaurants()) ) } catch (ex: Exception) { _restaurantsState.postValue( ResultState.Failure("Failed to fetch restaurants", ex) ) } } } }
测试类代码(原错误版本)
@ExperimentalCoroutinesApi class RestaurantsViewModelTest { private var repository = mockk<RestaurantsRepository>(relaxed = true) private var viewmodel = RestaurantsViewModel(repository) @get:Rule val mainDispatcherRule = MainDispatcherRule() @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @Test fun `restaurantsState LiveData is updated with the result from repository`() = runTest { val restaurants = listOf(mockk<Restaurant>(relaxed = true)) val success = ResultState.Success(restaurants) coEvery { repository.getRestaurants() } returns restaurants viewmodel.initialize() Assert.assertEquals(success, viewmodel.restaurantsState.value) } }
项目依赖配置
dependencies { implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.8.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation 'androidx.core:core-ktx:1.9.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation "com.squareup.retrofit2:converter-gson:2.9.0" implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.1" implementation "androidx.activity:activity-ktx:1.6.0" implementation "androidx.fragment:fragment-ktx:1.5.3" // testing // for runTest, CoroutineDispatcher testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0" // for InstantTaskExecutorRule testImplementation "androidx.arch.core:core-testing:2.1.0" testImplementation "io.mockk:mockk:1.13.2" implementation "com.google.dagger:hilt-android:2.44" kapt "com.google.dagger:hilt-compiler:2.44" }
报错信息
Exception in thread "Test worker" java.lang.IllegalStateException: 主调度器对应的模块初始化失败。测试环境下可使用kotlinx-coroutines-test模块中的Dispatchers.setMain方法解决
问题原因
测试类中viewmodel在类初始化阶段就被实例化,此时MainDispatcherRule的starting方法尚未执行(JUnit4规则的执行顺序是:Rule的starting → @Before方法 → 测试方法),ViewModel的init块会立即触发initialize()方法,调用viewModelScope.launch,此时主调度器未被替换为测试调度器,导致报错。
解决方案
1. 延迟ViewModel实例化时机
将ViewModel的初始化移至@Before注解的方法中,确保MainDispatcherRule已完成主调度器的设置后再创建ViewModel。
2. 确保协程执行完毕后再断言
在runTest中调用advanceUntilIdle(),等待所有测试调度器中的协程执行完成,保证LiveData的value已更新。
修改后的测试类代码
@ExperimentalCoroutinesApi class RestaurantsViewModelTest { private lateinit var repository: RestaurantsRepository private lateinit var viewmodel: RestaurantsViewModel @get:Rule val mainDispatcherRule = MainDispatcherRule() @get:Rule val instantTaskExecutorRule = InstantTaskExecutorRule() @Before fun setup() { repository = mockk(relaxed = true) // 此时MainDispatcherRule已完成主调度器设置 viewmodel = RestaurantsViewModel(repository) } @Test fun `restaurantsState LiveData is updated with the result from repository`() = runTest { val restaurants = listOf(mockk<Restaurant>(relaxed = true)) val success = ResultState.Success(restaurants) coEvery { repository.getRestaurants() } returns restaurants viewmodel.initialize() // 等待所有协程执行完毕 advanceUntilIdle() Assert.assertEquals(success, viewmodel.restaurantsState.value) } }
内容的提问来源于stack exchange,提问作者HukeLau_DABA
相关产品推荐
相关产品推荐

