從 runBlockingTest 到 runTest#

在 kotlinx-coroutines-test 早期版本中,最常見的測試入口是 runBlockingTest。它的功能是:

  • 建立一個會「跳過 delay」的 coroutine scope。
  • 在裡面可以呼叫 suspend 函式進行測試。
  • 預設使用會自動加速時間的 dispatcher。

但 runBlockingTest 後來被標記為 deprecated,原因包含:

  • 它對於「未完成的 coroutine」的判定過於嚴格,常常需要 hack 才能寫好測試。
  • JobSupervisorJob 等 scope 行為的整合不夠乾淨。
  • TestDispatcher 的設計不一致。

新版 API 改採 runTest,搭配 TestScope,行為更直觀。

runBlockingTest 範例(舊版)#

直接看一個 1500 毫秒會超時的測試:

@OptIn(ExperimentalCoroutinesApi::class)
@Test
internal fun downloadTest_Fail() = runBlockingTest {
    var isException = false
    try {
        day27.download(FakeService(1500))
    } catch (e: TimeoutCancellationException) {
        isException = true
    }
    assert(isException)
}

原本要等 1500 毫秒的 delay,在 runBlockingTest 中只花了大約 50 毫秒。時間被加速了 — 這就是測試函式庫最有價值的地方。

runTest 的基本用法#

新版的等價寫法:

@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun downloadTest() = runTest {
    val day27 = Day27()
    assertEquals("done", day27.download(FakeService(100)))
}

語法幾乎一致,但 runTest:

  • 回傳的是 TestResult(在 JVM 上等價於 Unit,在 JS 上是 Promise)。
  • 使用 TestScope 作為 coroutine scope。
  • 支援更細緻的時間控制,搭配 TestCoroutineScheduler 的 advanceTimeBy、advanceUntilIdle、currentTime。

runTest 的時間語意#

在 runTest 區塊中:

  • delay(1000) 不會真的等 1 秒,而是讓「虛擬時鐘」前進 1 秒。
  • 區塊預設會在所有 coroutine 完成後才結束。
  • 可以透過 scheduler 控制時間前進的時機。

例如:

@Test
fun timeAdvance() = runTest {
    val start = currentTime
    delay(2000)
    val end = currentTime
    assertEquals(2000L, end - start)
}

這裡的 currentTime 是 TestCoroutineScheduler 維護的虛擬時間,並非真實時間。

與 runBlocking 的差異#

項目runBlockingrunTest
適用場景一般程式進入點測試專用
delay 行為真的等待虛擬時間,立即前進
Scope 型別CoroutineScopeTestScope
取消行為一般與 TestDispatcher 整合

不要在測試裡用 runBlocking 包 suspend 函式來測時間相關邏輯,否則 CI 會被拖很慢。

從 runBlockingTest 遷移到 runTest 的要點#

  • runBlockingTest { ... } 換成 runTest { ... }
  • 若測試需要明確的 dispatcher,改用 StandardTestDispatcher 或 UnconfinedTestDispatcher 配合 runTest。
  • 取得 scheduler 的方式從 coroutineContext[TestCoroutineExceptionHandler] 改為 testScheduler
  • 直接使用 advanceTimeByadvanceUntilIdle 來控制時間流動。

小結#

  • runTest 是新版測試入口,取代被棄用的 runBlockingTest。
  • runTest 內部使用 TestScope 與 TestDispatcher,提供虛擬時間控制。
  • 寫測試時優先選用 runTest,避免時間相關邏輯拖慢測試。

原文出處#

  • 原書/iThome:https://ithelp.ithome.com.tw/articles/10276586