TestDispatcher 是什麼#

在 kotlinx-coroutines-test 新版 API 中,TestDispatcher 是測試 coroutine 時最關鍵的元件。它取代了舊版的 TestCoroutineDispatcher,並有兩個主要實作:

  • StandardTestDispatcher
  • UnconfinedTestDispatcher

兩者都會搭配同一個 TestCoroutineScheduler,讓我們可以控制虛擬時間。

TestCoroutineScheduler#

TestCoroutineScheduler 是負責管理虛擬時間的元件,提供幾個常用 API:

  • advanceTimeBy(delayMs):把虛擬時間往前推進指定毫秒。
  • advanceUntilIdle():把虛擬時間推到所有排程任務都完成。
  • currentTime:取得目前虛擬時間。
  • runCurrent():執行目前已到期的任務但不前進時間。

範例:

@Test
fun schedulerExample() = runTest {
    val results = mutableListOf<String>()
    launch {
        delay(1000)
        results += "1s"
        delay(1000)
        results += "2s"
    }

    advanceTimeBy(500)
    runCurrent()
    assertEquals(emptyList<String>(), results)

    advanceTimeBy(600) // 累計 1100ms
    runCurrent()
    assertEquals(listOf("1s"), results)

    advanceUntilIdle()
    assertEquals(listOf("1s", "2s"), results)
}

StandardTestDispatcher#

StandardTestDispatcher 的特色:

  • 啟動 coroutine 後,並不會立即執行內部程式碼。
  • 必須由我們手動呼叫 advanceTimeByadvanceUntilIdlerunCurrent 才會推進。
  • 適合需要精準驗證 coroutine 執行順序的測試。

範例:

@Test
fun standardExample() = runTest(StandardTestDispatcher()) {
    var value = 0
    launch {
        value = 1
    }
    // 此時 launch 還沒執行
    assertEquals(0, value)

    runCurrent()
    assertEquals(1, value)
}

UnconfinedTestDispatcher#

UnconfinedTestDispatcher 的特色:

  • 啟動 coroutine 後,會立即在當前執行緒執行,直到第一個 suspend 點。
  • 行為類似一般的 Dispatchers.Unconfined,但仍然支援虛擬時間。
  • 適合不需要關心執行順序、想要「直觀同步寫法」的測試。

範例:

@Test
fun unconfinedExample() = runTest(UnconfinedTestDispatcher()) {
    var value = 0
    launch {
        value = 1
    }
    // 已經執行
    assertEquals(1, value)
}

兩種 Dispatcher 的選擇#

需求建議使用
需要精準驗證執行順序、時序StandardTestDispatcher
想要寫起來像同步程式UnconfinedTestDispatcher
預設(不傳入時)runTest 預設搭配 StandardTestDispatcher

若測試只是想驗證最終結果(不在乎中間時序),UnconfinedTestDispatcher 通常更省心。

在 runTest 之外使用 TestDispatcher#

有時我們需要把 TestDispatcher 注入到被測元件,例如某個 ViewModel 內部 launch 用的 dispatcher:

@Test
fun viewModelTest() = runTest {
    val dispatcher = StandardTestDispatcher(testScheduler)
    val viewModel = MyViewModel(dispatcher)

    viewModel.load()
    advanceUntilIdle()

    assertEquals("loaded", viewModel.state)
}

重點是:

  • 自行建立的 TestDispatcher 要傳入同一個 testScheduler,這樣時間才會同步。
  • 如此一來,外層 runTest 的 advanceUntilIdle 也能推進內部 dispatcher 的工作。

與舊 API 的對應#

  • TestCoroutineDispatcher(舊)➡️ StandardTestDispatcher(新)。
  • runBlockingTest 內部(舊)➡️ runTest + TestDispatcher(新)。
  • pauseDispatcher / resumeDispatcher(舊)➡️ 改用 advanceTimeBy / runCurrent / advanceUntilIdle 控制。

小結#

  • TestDispatcher 是控制 coroutine 測試時序的核心。
  • StandardTestDispatcher 需要手動推進,適合精準時序測試。
  • UnconfinedTestDispatcher 立即執行,適合驗證最終結果。
  • 透過 TestCoroutineScheduler 的 advanceTimeBy、advanceUntilIdle、currentTime,可以在虛擬時間上自由前進。

原文出處#

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