测试工具包
kotlinx-coroutines-test:提供runTest、测试调度器。Turbine:Flow 测试工具。MockK/Mockito:模拟挂起函数。
Gradle 依赖:
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.1")
testImplementation("app.cash.turbine:turbine:1.1.0")
单元测试示例
@OptIn(ExperimentalCoroutinesApi::class)
class HomeViewModelTest {
private val dispatcher = StandardTestDispatcher()
@Before
fun setup() {
Dispatchers.setMain(dispatcher)
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun `refresh success`() = runTest {
val repository = FakeRepository(result = Result.success(listOf("A")))
val viewModel = HomeViewModel(repository)
viewModel.refresh()
dispatcher.scheduler.advanceUntilIdle()
assertEquals(listOf("A"), viewModel.state.value.items)
}
}
Flow 测试
@Test
fun `state emits loading then success`() = runTest {
val flow = repository.observeState()
flow.test {
assertEquals(State.Loading, awaitItem())
assertTrue(awaitItem() is State.Success)
cancelAndIgnoreRemainingEvents()
}
}
调试技巧
1. 协程调试器
- Android Studio 7.0+ 支持协程调试视图。
- 在
View > Tool Windows > Debug中查看 Coroutine 栈。
2. 日志追踪
val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
Timber.e(throwable, "Coroutine error")
}
viewModelScope.launch(coroutineExceptionHandler) {
// ...
}
3. 调度器标签
- 使用
Dispatchers.IO.limitedParallelism(n)控制线程数。 DebugProbes追踪协程状态(开发期间):
DebugProbes.install()
DebugProbes.dumpCoroutines(PrintWriter(System.out))
常见问题排查
| 问题 | 原因 | 排查步骤 |
|---|---|---|
| 协程未取消 | Job 未保存在作用域 | 检查 viewModelScope、launch 返回值 |
| 测试卡住 | 使用真实调度器 + 延迟 | 替换为 TestDispatcher,使用 advanceUntilIdle() |
| Flow 未触发 | 缺少收集者 | 检查 collect 位置,确认未忘记 collect |
| 线程切换异常 | 调度器混用 | 使用 withContext 明确线程切换 |
最佳实践
- 所有 ViewModel 协程统一使用
viewModelScope。 - 对
launch结果命名保存,用于取消或测试。 - 在测试中使用
runTest+StandardTestDispatcher控制时间。 - 使用
assertThrows验证错误场景,确保异常路径覆盖。
实战案例:runTest 是 Dart 没有的东西
Dart 写异步测试用 fake_async 包,思路是”虚拟时钟”;Kotlin 的 runTest + StandardTestDispatcher 也是虚拟时钟,但做得更彻底——连 delay() 都被接管了。
我在 Flutter 项目里测过一个带防抖的搜索框:用户停 500ms 才发请求。Dart 的写法是用 FakeAsync().run((async) { async.elapse(Duration(milliseconds: 500)); ... }),能用,但 FakeAsync 和真实的 Timer 混用时经常翻车(Zone 嵌套问题)。
Kotlin 这套更干净:runTest 直接把协程的调度器换成测试调度器,advanceUntilIdle() 一把梭把所有挂起任务跑完,不用操心 Zone。代价是心智负担——Dispatchers.setMain(dispatcher) / resetMain() 这对 setup/teardown 必须配对写,漏了 resetMain() 会导致后面的测试用例互相污染,这个坑官方文档的测试指南里专门强调过。
一句话:Kotlin 协程测试的哲学是”把整个世界换成假的”,比 Dart 的”只把时钟换成假的”更彻底,但也要求你更自律。
参考:Kotlin 官方文档《Testing Kotlin coroutines on Android》
总结
协程调试需要结合 IDE 工具与日志追踪;测试阶段通过 kotlinx-coroutines-test 可以模拟时间、线程环境。建立标准调试流程与测试模板,有助于快速定位并发问题,提升协程相关代码的可靠性。
