Kotlin runTest测试含delay的非挂起inc()方法不生效问题
核心问题
你当前测试失败的根源有两点:
- 生产代码中
MyViewModel的协程上下文硬编码依赖Dispatchers.IO,测试时仅通过Dispatchers.setMain替换主线程调度器,无法接管IO调度器上运行的协程,测试框架无法控制这部分协程的执行时序。 inc()是普通非挂起函数,内部通过launch启动的是异步协程;你在外层调用join()仅能等待inc()方法本身返回,无法等待inc()内部启动的协程执行完成,断言触发时内部的delay(100)逻辑还未执行。
正确实现方案
因为Incrementer接口固定无法修改,我们不需要改动inc()的方法签名,只需要通过调度器依赖注入解决硬编码问题,配合测试调度器的时序控制即可完成测试,这也是JVM/Android协程代码的标准实践。
1. 调整生产代码(不修改接口定义)
将硬编码的Dispatchers.IO改为构造参数传入,生产环境默认使用Dispatchers.IO,测试环境可替换为测试调度器:
interface Incrementer { fun inc() } class MyViewModel( // 调度器作为构造参数注入,生产环境默认用IO调度器 private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : Incrementer, CoroutineScope { override val coroutineContext: CoroutineContext get() = dispatcher private val _number = MutableStateFlow(0) fun getNumber(): StateFlow<Int> = _number.asStateFlow() override fun inc() { launch { delay(100) _number.tryEmit(1) } } }
2. 编写测试代码
测试时将runTest内置的测试调度器传入ViewModel,调用inc()后通过advanceUntilIdle()等待测试调度器上所有协程执行完成,再执行断言:
class IncTest { @Test fun incrementOnce() = runTest { // 传入测试调度器,接管ViewModel所有协程的执行 val viewModel = MyViewModel(dispatcher = StandardTestDispatcher(testScheduler)) val results = mutableListOf<Int>() val resultJob = viewModel.getNumber() .onEach(results::add) .launchIn(UnconfinedTestDispatcher(testScheduler)) // 直接调用普通方法inc() viewModel.inc() // 关键:推进测试时间,直到所有协程任务执行完毕(包含100ms的delay逻辑) advanceUntilIdle() assertEquals(listOf(0, 1), results) resultJob.cancel() } }
注意事项
- 不需要再全局调用
setMain/resetMain,只要所有协程都使用注入的测试调度器,就能完全掌控执行时序,避免全局配置带来的测试交叉污染。 - 绝对不要在调用
inc()后立刻断言:非挂起函数的调用返回仅代表内部协程被提交到调度器队列,不代表协程逻辑执行完成,必须等待调度器空闲后再做校验。 - 不要额外用
launch包裹viewModel.inc()调用再join,这种写法只能等待inc()方法本身返回,无法等待内部异步协程执行,没有实际意义。
内容的提问来源于stack exchange,提问作者Robert
相关产品推荐
相关产品推荐

