Skip to content

协程单元测试 (runTest)

源:kotlinx-coroutines-test 指南

依赖配置

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()
    }
}