為什麼測試 Coroutine 需要特別的工具#
Coroutine 是非同步程式的解決方案。當我們把耗時任務放進 suspend 函式,正常情況下需要在 coroutine scope 中執行,例如使用 launch 或 async。在程式進入點,我們常常會使用 runBlocking 建立 coroutine scope,作為一般函式與 coroutine 世界的橋樑。
但當我們進入測試情境時,問題就來了:
- suspend 函式必須在 coroutine 中執行。
- 若直接在測試裡使用 runBlocking 包住 suspend 函式,遇到
delay、withTimeout之類的時間相關 API,測試會真的等那麼久。 - 一個有 30 秒延遲邏輯的測試,在 CI 上會真的耗掉 30 秒,這對快速回饋是致命的。
因此 Kotlin 官方提供了一個專門用來測試 coroutine 的函式庫 — kotlinx-coroutines-test。
kotlinx-coroutines-test 是什麼#
kotlinx-coroutines-test 是 JetBrains 為了測試 coroutine 行為所提供的函式庫,它的核心目標包含:
- 提供能在 JUnit 測試中啟動 coroutine 的入口。
- 控制虛擬時間(virtual time),讓
delay不必真的等。 - 控制排程行為,方便驗證 coroutine 的執行順序與時序。
加入相依性#
在 Gradle 專案中加入測試相依:
dependencies {
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.5.2")
}後續版本(1.6.0 以後)已經正式提供
runTest、TestDispatcher等新 API。本系列以 1.5.2 為例介紹核心概念,但實務上建議使用最新版本。
函式庫提供的主要元件#
kotlinx-coroutines-test 提供了幾組重要工具,後續章節會逐一介紹:
- runBlockingTest(已棄用):早期版本最常見的測試入口。
- runTest:新版取代 runBlockingTest 的測試入口,搭配 TestScope。
- TestScope:在 runTest 中執行測試時所使用的 coroutine scope。
- TestDispatcher:可控制虛擬時間的 dispatcher,包含 StandardTestDispatcher 與 UnconfinedTestDispatcher。
- TestCoroutineScheduler:管理虛擬時間,可配合 advanceTimeBy、advanceUntilIdle、currentTime 等 API 操作。
- Dispatchers.setMain / Dispatchers.resetMain:讓 Main 執行緒(Main Thread)能在 JVM 測試環境下被替換成 TestDispatcher。
一個簡單的範例#
我們有一個 Day27 類別,內部呼叫 download,並用 withTimeout 控制超時:
class Day27 {
suspend fun download(service: IService): String {
withTimeout(1000) {
return@withTimeout service.download()
}
return "fail"
}
}
interface IService {
suspend fun download(): String
}這裡刻意把 IService 抽成介面,方便測試時注入假實作。
對應的假服務:
class FakeService(private val timeout: Long) : IService {
override suspend fun download(): String {
delay(timeout)
return "done"
}
}對應的測試(以舊版 runBlockingTest 為例):
internal class Day27Test {
private lateinit var day27: Day27
@BeforeEach
internal fun setUp() {
day27 = Day27()
}
@OptIn(ExperimentalCoroutinesApi::class)
@Test
internal fun downloadTest() = runBlockingTest {
kotlin.test.assertEquals("done", day27.download(FakeService(100)))
}
}雖然 FakeService 內部有 delay,但因為跑在 runBlockingTest 中,測試會跳過真實時間直接前進,幾毫秒就跑完。
小結#
- kotlinx-coroutines-test 是 Kotlin 官方提供的 coroutine 測試函式庫。
- 它解決了「直接用 runBlocking 測試 suspend 函式會真的耗時」的痛點。
- 核心概念是「虛擬時間」與「可控排程」。
- 後續章節會分別介紹 runTest、TestDispatcher、以及 Main 執行緒測試的處理方式。
本系列原文撰寫時,許多 API 仍標記為
ExperimentalCoroutinesApi。新版多已穩定,但語意大致相同。
原文出處#
- 原書/iThome:https://ithelp.ithome.com.tw/articles/10276586