协程单元测试 (runTest)
依赖配置
runTest 等 API 位于专用的测试库中,需在 build.gradle 中添加依赖:
kotlin
dependencies {
// 基础协程库
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
// 协程测试库 (包含 runTest)
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}黄金入口:runTest
严禁在测试中使用 runBlocking! 请始终使用标准 API runTest。 它会创建一个 TestScope,并且内部的时间是虚拟的(Virtual Time)。
kotlin
@Test
fun testDelay() = runTest {
val startTime = currentTime
delay(10000) // 虚拟时间流逝 10秒,但真实执行只耗时几毫秒
assertEquals(10000, currentTime - startTime)
}调度器:Standard vs Unconfined
在 runTest 内部,默认使用 StandardTestDispatcher。
StandardTestDispatcher (默认)
行为:新启动的协程默认是挂起的,不会自动执行。你需要显式推进时间。
kotlin
@Test
fun testStandard() = runTest {
var state = "Init"
launch { state = "Done" }
assertEquals("Init", state) // 协程还没跑呢!
runCurrent() // 手动推一下
assertEquals("Done", state)
}UnconfinedTestDispatcher
行为:像 Unconfined 一样,立即在当前线程执行,不需要手动推进。适用于简单的 ViewModel 测试。
kotlin
@Test
fun testUnconfined() = runTest(UnconfinedTestDispatcher()) {
var state = "Init"
launch { state = "Done" }
assertEquals("Done", state) // 立即执行了
}测试 Flow:backgroundScope
测试永不结束的热流(如 StateFlow)时,如果在 runTest 块内直接 collect,测试会永远挂起直到超时。 解决方案:使用 backgroundScope 在测试结束时自动取消协程。
kotlin
@Test
fun testStateFlow() = runTest {
val viewModel = MyViewModel()
val results = mutableListOf<String>()
// 启动收集器,绑定到 backgroundScope
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.uiState.collect { results.add(it) }
}
// 执行操作
viewModel.loadData()
// 验证
assertEquals(listOf("Loading", "Success"), results)
} // backgroundScope 在此处自动取消,测试顺利结束Android 主线程调度器替换
Android 代码中用到了 Dispatchers.Main,在单元测试(JVM)环境中会报错。需要使用 MainDispatcherRule 进行动态替换。
kotlin
class MainDispatcherRule @OptIn(ExperimentalCoroutinesApi::class) constructor(
val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}