You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 00:07:21