SuperAPI AI 工具集

kp-testing-coroutines

测试与质量

查看项目

更新于 10-16 16:26

技能介绍

用途

该技能用于为使用协程的代码编写测试。当需要为 suspend fun 或 Flow 编写单元测试、测试时间相关行为(超时、延迟、debounce、sample)而不实际等待、测试并发协程及其交互,或者替换缓慢、不稳定的 runBlocking 测试设置时使用。

不适用的情况:代码实际上并未挂起、不涉及协程时,应使用普通测试;确实需要真实时间与线程的集成测试,runBlocking 仍可能适用(少见)。

主要能力

runTest { … } 是 kotlinx-coroutines-test 提供的规范入口点。它把测试体作为协程运行,并带有具备 TestDispatcher 和虚拟时钟的 TestScope。示例:

@Test fun loads user() = runTest { val repo = FakeUserRepo() val sut = UserUseCase(repo) val user = sut.load(UserId("1")) assertEquals("Alice", user.name) }

虚拟时间:runTest 内部的 delay(10.minutes) 会立即完成,runTest 会跳过延迟并推进虚拟时钟。无论协程内经过多少时间,测试都在毫秒级完成。这是测试超时、防抖、轮询、定时任务的关键能力。

两种 TestDispatcher:

StandardTestDispatcher(runTest 的默认值)——在作用域中启动的协程会被排队,而不是立即执行。使用 advanceUntilIdle() 运行它们,或用 advanceTimeBy(d) 推进时间并运行该时间窗口内调度的内容。它反映生产调度器的工作方式,并保持顺序。

UnconfinedTestDispatcher——立即执行。每个新协程在调用线程上立即运行,直到其挂起。对简单测试更容易,但对真实调度器行为的还原度较低。当执行顺序无关紧要时使用。

使用方式

向生产代码注入调度器:生产代码应把调度器作为构造函数参数接收(或使用 CoroutineDispatcherProvider 间接层)。测试传入测试调度器,生产环境传入 Dispatchers.IO / Default。

class UserUseCase(private val repo: UserRepo, private val io: CoroutineDispatcher) { suspend fun load(id: UserId): User = withContext(io) { repo.find(id) } } // Test: pass StandardTestDispatcher() instead of Dispatchers.IO

Dispatchers.setMain(testDispatcher):对于硬编码 Dispatchers.Main 的代码(主要见于 Android),在测试中替换它:

@Before fun setUp() { Dispatchers.setMain(StandardTestDispatcher()) } @After fun tearDown() { Dispatchers.resetMain() }

测试 Flow:使用 flow.toList() 收集有限流,或使用 Turbine(app.cash.turbine,一个流行的第三方库)对热流和随时间产生的发射进行更丰富的断言 DSL。

决策启发式

默认使用 runTest + StandardTestDispatcher。仅当不关心顺序依赖且立即执行能简化测试时,才切换到 UnconfinedTestDispatcher。把调度器注入生产代码,不要在业务逻辑中硬编码 Dispatchers.IO。积极使用虚拟时间——测试中的 delay(10.minutes) 应在毫秒内运行;如果测试真的在休眠,说明没有正确进入 runTest。测试代码优先使用 runTest 而不是 runBlocking——runBlocking 会阻塞真实线程,且不遵循测试作用域。

适用场景

适用于为 suspend fun 或 Flow 编写单元测试、测试时间相关行为、测试并发协程交互,以及替换缓慢或不稳定的 runBlocking 测试设置。

限制与反模式

反模式包括:在测试中使用 runBlocking(缓慢、无虚拟时间、无测试作用域取消);在生产代码中硬编码 Dispatchers.IO / Dispatchers.Main 导致测试无法替换(应让调度器可注入);在挂起代码中使用真实的 Thread.sleep(不可取消,破坏虚拟时间);在 runTest 内部使用 GlobalScope.launch(协程逃逸出测试作用域,失败不会被观察到);使用 StandardTestDispatcher 时忘记 advanceUntilIdle()(排队的协程永远不会运行,测试会错误通过)。

相关技能:kp-coroutine-scope(TestScope 是用于测试的 CoroutineScope)、kp-dispatchers(TestDispatcher 在测试期间替代的对象)、kp-flow-operators(通过收集或 Turbine 测试 flow 管道)、kp-anti-patterns(runBlocking 与硬编码调度器的坏味道)。

参考资料:Kotlin Coroutines(Marcin Moskała)——关于测试 Kotlin 协程的章节;kotlinx-coroutines-test 官方文档;Turbine(app.cash.turbine)——流行的 Flow 测试库。