应使用 runtest 而非 runblockingtest(已弃用)或 runblocking:前者自动虚拟时间、捕获异常、等待全部协程完成;后者不替换调度器、真实等待 delay、静默吞异常且无父子协程清理机制。

直接用 runTest,别碰 runBlockingTest(已弃用)或裸写 runBlocking。 否则测试会漏异常、时间不虚拟、作用域不自动清理,甚至在 CI 上随机失败。
为什么不能用 runBlocking 测试协程
它只是阻塞当前线程等待协程结束,但:
• 不替换默认调度器,Dispatchers.Main 或 Dispatchers.IO 仍走真实线程池
• delay(1000) 会真等 1 秒,拖慢整个测试套件
• 未捕获的协程异常会被静默吞掉,测试通过但逻辑实际已崩
• 没有父子协程自动等待机制,子协程可能被提前裁剪
runTest 必须配的依赖和配置
确保 build.gradle.kts 中有:
dependencies {
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0")
testImplementation("org.junit.jupiter:junit-jupiter:5.10.2")
}
不需要手动设置 TestCoroutineDispatcher 或调用 Dispatchers.setMain() —— runTest 内部已自动完成这些。
常见错误写法及修正
以下写法看似能跑通,但埋了坑:
-
错:用
GlobalScope.launch+join()—— 测试作用域外启动协程,join()只等一个,其他子协程不受控 -
错:在
@Test函数里直接调suspend函数 —— 编译报错 “Suspend function should be called only from a coroutine” -
错:用
runBlocking { launch { ... } }—— 子协程没 await,测试函数返回时它还在后台跑 -
对:统一用
runTest { ... },所有挂起调用、async、launch都在它的作用域内,自动等待全部完成
示例:
@Test
fun networkCall_returnsResult() = runTest {
val repo = TitleRepository(FakeNetwork("OK"), FakeDao())
val result = repo.refreshTitle() // 直接调 suspend 函数
assertEquals("OK", result)
}
延迟和时间控制必须用虚拟时钟
如果被测代码含 delay(3000) 或 withTimeout:
• runTest 默认启用虚拟时间,delay 不会真实休眠
• 用 advanceTimeBy(3000) 手动快进,验证中间状态
• 用 advanceUntilIdle() 等待所有挂起操作自然结束
• 别依赖 Thread.sleep 或真实计时器断言,那会破坏确定性
真正容易被忽略的是:即使你写了 runTest,如果测试函数签名用了 suspend(比如 @Test suspend fun ...),JUnit 会跳过它——JVM 测试运行器根本不认识协程。必须保持函数是普通 @Test fun,靠 = runTest { ... } 尾置调用来承载协程上下文。











