Kotestコルーチン(Coroutines)

Kotestでコルーチンをテストする方法について説明する。

コルーチンディスパッチャのテスト

TestDispatcherは、開発者が仮想クロックを制御し、遅延をスキップできるようにするkotlinx-coroutines-testモジュールが提供する特別なCoroutineDispatcherである。

TestDispatcherは、開発者が仮想クロックを制御し、遅延をスキップできるようにするkotlinx-coroutines-testモジュールが提供する特別なCoroutineDispatcherである。

TestDispatcherは次の操作をサポートする。

  • currentTimeは現在の仮想時刻を取得する。
  • runCurrent()はこの仮想時刻に予約されたタスクを実行する。
  • advanceUntilIdle()は保留中のタスクがなくなるまで、キュー内のすべてのタスクを実行する。
  • advanceTimeBy(timeDelta)は現在の仮想時刻がtimeDeltaだけ進むまで、キュー内のタスクを実行する。

テストでTestDispatcherを使用するには、テスト設定でcoroutineTestScopeを有効にする。

package com.devkuma.kotest.tutorial.coroutines.ex1

import io.kotest.core.spec.style.FunSpec

class TestDispatcherTest : FunSpec() {
    init {
        test("foo").config(coroutineTestScope = true) {
            // this test will run with a test dispatcher
        }
    }
}

このテスト内では、testCoroutineSchedulerというExtensionを通じてスケジューラへのハンドルを取得できる。このスケジューラを使って時間を操作できる。

import io.kotest.core.test.testCoroutineScheduler

class TestDispatcherTest : FunSpec() {
   init {
      test("advance time").config(coroutineTestScope = true) {
        val duration = 1.days
        // launch a coroutine that would normally sleep for 1 day
        launch {
          delay(duration.inWholeMilliseconds)
        }
        // move the clock on and the delay in the above coroutine will finish immediately.
        testCoroutineScheduler.advanceTimeBy(duration.inWholeMilliseconds)
        val currentTime = testCoroutineScheduler.currentTime
      }
   }
}

SpecレベルでcoroutineTestScopeをtrueに設定すると、Spec内のすべてのテストに対してテストディスパッチャを有効にできる。

class TestDispatcherTest : FunSpec() {
   init {
      coroutineTestScope = true
      test("this test uses a test dispatcher") {
      }
      test("and so does this test!") {
      }
   }
}

最後に、ProjectConfigを使用してモジュール内のすべてのテストに対してテストディスパッチャを有効にできる。

class ProjectConfig : AbstractProjectConfig() {
  override var coroutineTestScope = true
}

コルーチンデバッグ

kotlinx-coroutines-debugは、JVM上のコルーチンに対するデバッグ機能を提供するモジュールである。有効にすると、デバッグエージェントがByteBuddyによってインストールされ、コルーチンが生成、開始、一時停止、再開されるときの情報を取得する。

Kotestはテストごとにデバッグを有効にする機能を提供している。テスト設定でcoroutineDebugProbeを有効にすると、この機能を使用できる。

この機能を有効にすると、テスト内で開始されたすべてのコルーチンは、テスト完了後または例外発生直後にcoroutine dumpへ含まれる。

class CoroutineDebugging : FunSpec() {
   init {
      test("foo").config(coroutineDebugProbes = true) {
         someMethodThatLaunchesACoroutine() // launches a new coroutine
      }
   }
}

coroutine dumpは次のようになる。

Coroutines dump 2021/11/27 22:17:43

Coroutine DeferredCoroutine{Active}@71f1906, state: CREATED
    (Coroutine creation stacktrace)
    at kotlin.coroutines.intrinsics.IntrinsicsKt__IntrinsicsJvmKt.createCoroutineUnintercepted(IntrinsicsJvm.kt:122)
    at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:30)
    at kotlinx.coroutines.BuildersKt__Builders_commonKt.async$default(Builders.common.kt:82)
    at kotlinx.coroutines.BuildersKt.async$default(Unknown Source)
    at com.sksamuel.kotest.engine.coroutines.Wibble$1.invokeSuspend(CoroutineDebugTest.kt:37)
    at com.sksamuel.kotest.engine.coroutines.Wibble$1.invoke(CoroutineDebugTest.kt)

Specレベル構成

Spec内でcoroutineDebugProbes設定を再定義すると、Spec内のすべてのテストに対してコルーチンデバッグを有効にできる。

class CoroutineDebugging : FunSpec() {
  init {

    coroutineDebugProbes = true

    test("foo") {
      // debugging enabled here
    }

    test("bar") {
      // debugging enabled here
    }

  }
}

プロジェクト全体構成

ProjectConfigを使用して、プロジェクト内のすべてのテストに対してコルーチンデバッグを有効にできる。

class ProjectConfig : AbstractProjectConfig() {
  override val coroutineDebugProbes = true
}

参照