Android单元测试:调用含viewModelScope的函数引发竞态条件
解决ViewModel协程测试的竞态条件问题
核心问题
你的测试代码中,viewModelScope默认绑定的是应用原生的Main调度器,而非你配置的UnconfinedTestDispatcher,导致runTest无法管控该协程的执行时机,断言触发时协程还未执行完毕。另外,原代码提前在类初始化阶段创建ViewModel,此时MainCoroutineRule尚未生效,viewModelScope已绑定默认调度器,进一步加剧了竞态问题。
可行解决方案
方案1:构造函数注入可控CoroutineScope(推荐)
修改ViewModel,通过构造函数注入CoroutineScope,避免依赖默认的viewModelScope,测试时直接传入测试用的Scope:
// ViewModel代码 class MyViewModel( private val coroutineScope: CoroutineScope = viewModelScope ) : ViewModel() { var flag = false fun doSomething() { coroutineScope.launch { runATask() flag = true } } private suspend fun runATask() { // 模拟业务耗时逻辑 delay(100) } }
对应的测试代码:
@OptIn(ExperimentalCoroutinesApi::class) class MyViewModelTest { @Test fun test_doSomething_setsFlagToTrue() = runTest { // 将当前TestScope传入ViewModel,确保协程受测试调度器管控 val viewModel = MyViewModel(this) viewModel.doSomething() // runTest会自动推进测试调度器,无需额外等待 assert(viewModel.flag == true) } }
方案2:兼容老代码,强制替换Main调度器
如果无法修改ViewModel构造函数,可通过规则替换Main调度器,并在测试中推进协程执行:
@OptIn(ExperimentalCoroutinesApi::class) class MyViewModelTest { @get:Rule val mainDispatcherRule = TestDispatcherRule() @Test fun test_doSomething_setsFlagToTrue() = runTest { // 在Rule生效后创建ViewModel,确保viewModelScope使用测试调度器 val viewModel = MyViewModel() viewModel.doSomething() // 推进调度器,执行完所有挂起任务 advanceUntilIdle() assert(viewModel.flag == true) } } // 自定义测试调度器规则 @OptIn(ExperimentalCoroutinesApi::class) class TestDispatcherRule : TestWatcher() { private val testDispatcher = UnconfinedTestDispatcher() override fun starting(description: Description) { super.starting(description) Dispatchers.setMain(testDispatcher) } override fun finished(description: Description) { super.finished(description) Dispatchers.resetMain() } }
关键注意事项
- 必须在测试规则生效后创建ViewModel,避免
viewModelScope提前绑定默认调度器。 - 若测试中存在
delay()等挂起逻辑,需调用advanceUntilIdle()推进测试调度器时间,确保协程执行完成。 - 优先使用构造函数注入的方式,让ViewModel的协程依赖更透明,测试扩展性更强。
内容的提问来源于stack exchange,提问作者cboomer
相关产品推荐
相关产品推荐

