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

无需注入协程调度器即可实现代码可测试的替代方案

问题根源

你当前测试失败的核心原因是MainCoroutineRule仅替换了Main调度器,而你代码中硬编码的flowOn(Dispatchers.IO)会让channelFlow内部逻辑运行在真实的IO线程池,测试中的advanceTimeBy只能控制测试调度器的虚拟时间,无法干预真实IO线程中的delay逻辑,因此执行断言时Flow还没有发射任何数据,拿到的自然是空数组。

最优解决方案(低侵入注入调度器,官方推荐)

你不需要为了测试在所有业务代码中处处注入调度器,仅需给相关类增加带默认值的构造参数即可,生产环境完全无需修改现有调用逻辑:

  1. 修改数据提供类,增加调度器默认参数
class NumbersProvider(
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) {
    val numbersFlow: Flow<Int> = channelFlow {
        var i = 0
        while (i < 10) {
            delay(100)
            send(i)
            i++
        }
    }.flowOn(ioDispatcher)
}
  1. 修改ViewModel,增加调度器默认参数
class NumbersViewModel(
    private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
    private val dataProvider: NumbersProvider = NumbersProvider(ioDispatcher)
): ViewModel() {
    private val _numbers: MutableLiveData<IntArray> = MutableLiveData(IntArray(0))
    val numbers: LiveData<IntArray> = _numbers

    fun startCollecting() {
        viewModelScope.launch(Dispatchers.Main) {
            dataProvider.numbersFlow
                .onStart { println("start") }
                .onCompletion { println("end") }
                .catch { exception ->  println(exception.message.orEmpty())}
                .collect { data -> onDataRead(data) }
        }
    }

    fun onDataRead(data: Int) {
        _numbers.value = _numbers.value?.plus(data)
    }
}
  1. 测试时传入测试调度器即可
@Before
fun setUp() {
    viewModel = NumbersViewModel(ioDispatcher = mainCoroutineRule.dispatcher)
}

同时建议在测试的advanceTimeBy后调用runCurrent(),确保所有待执行任务都运行完成再执行断言:

@Test
fun `provider_provides_10_values`() {
    viewModel.startCollecting()
    mainCoroutineRule.advanceTimeBy(2000)
    mainCoroutineRule.runCurrent()
    val numbers = viewModel.numbers.value
    assertThat(numbers?.size).isEqualTo(10)
}

该方案仅增加了两个默认参数,对业务代码侵入性极低,也符合协程测试的官方最佳实践,不需要引入全局单例。

零侵入替代方案(仅测试修改)

如果你完全不想修改任何业务代码,可以使用MockK的静态Mock能力直接在测试中替换Dispatchers.IO:

  1. 引入MockK测试依赖
  2. 测试类增加静态Mock逻辑
@Before
fun setUp() {
    mockkStatic(Dispatchers::class)
    every { Dispatchers.IO } returns mainCoroutineRule.dispatcher
    viewModel = NumbersViewModel()
}

@After
fun tearDown() {
    unmockkStatic(Dispatchers::class)
}

该方案无需修改任何业务代码,缺点是静态Mock会带来少量测试性能开销,多测试类并行执行时需要注意Mock的生命周期管理。

内容的提问来源于stack exchange,提问作者user3921796

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.01 07:45:02