用途
该技能用于为使用协程的代码编写测试。当需要为 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 测试库。