跳转到正文
YGJoon 技术博客

Android Kotlin 协程测试与调试技巧

发布于:
·771 字 · 约 2 分钟·作者:
协程测试与排障:runTest 虚拟时间、Turbine 测 Flow、协程调试器看调用栈,解决 flaky 测试和泄漏两大痛点。
Android Kotlin 协程测试与调试技巧文章封面
目录

测试工具包

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. 协程调试器

2. 日志追踪

val coroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
    Timber.e(throwable, "Coroutine error")
}

viewModelScope.launch(coroutineExceptionHandler) {
    // ...
}

3. 调度器标签

DebugProbes.install()
DebugProbes.dumpCoroutines(PrintWriter(System.out))

常见问题排查

问题原因排查步骤
协程未取消Job 未保存在作用域检查 viewModelScope、launch 返回值
测试卡住使用真实调度器 + 延迟替换为 TestDispatcher,使用 advanceUntilIdle()
Flow 未触发缺少收集者检查 collect 位置,确认未忘记 collect
线程切换异常调度器混用使用 withContext 明确线程切换

最佳实践

  1. 所有 ViewModel 协程统一使用 viewModelScope。
  2. 对 launch 结果命名保存,用于取消或测试。
  3. 在测试中使用 runTest + StandardTestDispatcher 控制时间。
  4. 使用 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 可以模拟时间、线程环境。建立标准调试流程与测试模板,有助于快速定位并发问题,提升协程相关代码的可靠性。


分享这篇文章:


相关文章

上一篇
Android Kotlin Channel 与并发通信模式
下一篇
Android Kotlin Multiplatform 入门