Upgrade to Pro — share decks privately, control downloads, hide ads and more …

suspend関数のテストの疑問調べてみた

Avatar for usuiat usuiat
October 23, 2026

 suspend関数のテストの疑問調べてみた

DroidKaigi.collect { #31@Nagoya }
https://droidkaigi.connpass.com/event/407026/

Avatar for usuiat

usuiat

October 23, 2026

More Decks by usuiat

Other Decks in Technology

Transcript

  1. About Me Atsushi Usui 臼井 篤志 Android Engineer Cybozu, Inc.

    サイボウズ株式会社 Book『詳解 Jetpack Compose』 https://gihyo.jp/book/2024/978-4-297-14488-3 @usuiat Library “Zoomable” https://github.com/usuiat/Zoomable
  2. suspend関数のテスト、runTestで書きますよね テスト対象 suspend fun addAfterDelay(a: Int, b: Int): Int {

    delay(10.seconds) return a + b } テスト @Test fun test_with_runTest() = runTest { assertEquals(3, addAfterDelay(1, 2)) }
  3. runBlockingでは10秒かかるテストが、 runTestでは一瞬で完了する runTest @Test fun test_with_runTest() = runTest { assertEquals(3,

    addAfterDelay(1, 2)) } runBlocking @Test fun test_with_runBlocking() = runBlocking { assertEquals(3, addAfterDelay(1, 2)) }
  4. delayのしくみ // Delay.kt(kotlinx.coroutines 1.11.0、抜粋) public suspend fun delay(timeMillis: Long) {

    return suspendCancellableCoroutine { cont -> // Contextに入っているDelayの実装に任せる cont.context.delay .scheduleResumeAfterDelay(timeMillis, cont) } } delayはCoroutineContextに含 まれているDelayインター フェースの実装に処理を委ね ている • delay自身は中断するだけ • いつ再開するかはDelayの 実装次第 public interface Delay { fun scheduleResumeAfterDelay( timeMillis: Long, continuation: CancellableContinuation<Unit> ) }
  5. runTestはテスト用のDelayをCoroutineContextにセットする CoroutineContext(runTestがセット) delay(10.seconds) StandardTestDispatcher TestCoroutineScheduler Delayを実装 イベントと仮想時刻を管理 • runTestはCoroutineContextにStandardTestDispatcherとTestCoroutineSchedulerを セットする

    • StandardTestDispatcher(の親クラスTestDispatcher)がDelayを実装している • runTest内でdelay(10.seconds)を呼ぶと、TestCoroutineSchedulerに「10秒後に再開 する」イベントが登録される
  6. TestCoroutineSchedulerは仮想時刻で動く // TestCoroutineScheduler.kt(抜粋) internal fun tryRunNextTaskUnless(...): Boolean { val event

    = synchronized(lock) { val event = events.removeFirstOrNull() ?: return false currentTime = event.time // 仮想時刻を進めるだけ event } event.dispatcher.processEvent(event.marker) return true // ↑ すぐに再開 } TestCoroutineSchedulerの次のイベントを実行する処理 イベントの時刻まで実際 に待つのではなく、仮想 時刻を進めてすぐにイベ ントを実行する 仮想時刻の実態は、単純なカウ ンター → テストが高速に実 行できる
  7. 公式のベストプラクティスでもDispatcherをDIしている // developer.android.com「コルーチンに関するベストプラクティス」より class NewsRepository( private val defaultDispatcher: CoroutineDispatcher =

    Dispatchers.Default ) { suspend fun loadNews() = withContext(defaultDispatcher) { /* ... */ } } https://developer.android.com/kotlin/coroutines/coroutines-best-practices?hl=ja
  8. DIできないと、delayがスキップされない class Sample { suspend fun add(a: Int, b: Int):

    Int { return withContext(Dispatchers.Default) { delay(10.seconds) // ハードコード↑ a + b } } } @Test fun addTest() = runTest { val sample = Sample() assertEquals(3, sample.add(1, 2)) } このテストは通るが、10秒かかる
  9. DIしてTestDispatcherを渡すと一瞬 class Sample( private val dispatcher: CoroutineDispatcher = Dispatchers.Default )

    { suspend fun add(a: Int, b: Int): Int { return withContext(dispatcher) { delay(10.seconds) a + b } } } @Test fun addTest() = runTest { // スケジューラーを共有して同じ仮想時間で管理 val dispatcher = StandardTestDispatcher(testScheduler) val sample = Sample(dispatcher) assertEquals(3, sample.add(1, 2)) } withContextで使うDispatcherを外部 から渡せるようにし、テストでは StandardTestDispatcherを渡せば、 内側もdelayがスキップされる