Kotest Coroutines

This page explains how to test coroutines in Kotest.

Testing Coroutine Dispatchers

TestDispatcher is a special CoroutineDispatcher provided by the kotlinx-coroutines-test module that lets developers control a virtual clock and skip delays.

TestDispatcher is a special CoroutineDispatcher provided by the kotlinx-coroutines-test module, which allows developers to control a virtual clock and skip delays.

TestDispatcher supports the following operations.

  • currentTime gets the current virtual time.
  • runCurrent() runs tasks scheduled at the current virtual time.
  • advanceUntilIdle() runs all queued tasks until there are no more pending tasks.
  • advanceTimeBy(timeDelta) runs queued tasks until the current virtual time has advanced by timeDelta.

To use TestDispatcher in a test, enable coroutineTestScope in the test configuration:

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
        }
    }
}

Inside this test, you can retrieve a handle to the scheduler through an extension named testCoroutineScheduler. You can use this scheduler to manipulate time:

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
      }
   }
}

You can enable the test dispatcher for all tests in a spec by setting coroutineTestScope to true at the spec level:

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

Finally, you can enable the test dispatcher for all tests in a module by using ProjectConfig:

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

Coroutine Debugging

kotlinx-coroutines-debug is a module that provides debugging support for coroutines on the JVM. When enabled, a debug agent is installed by ByteBuddy and captures information when coroutines are created, started, suspended, and resumed.

Kotest provides a feature for enabling debugging per test. You can use it by enabling coroutineDebugProbe in the test configuration.

When this feature is enabled, all coroutines started inside the test are included in a coroutine dump after the test completes or as soon as an exception occurs.

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

The coroutine dump looks like this:

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-level Configuration

You can enable coroutine debugging for all tests in a spec by overriding the coroutineDebugProbes setting inside the spec:

class CoroutineDebugging : FunSpec() {
  init {

    coroutineDebugProbes = true

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

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

  }
}

Project-wide Configuration

You can enable coroutine debugging for all tests in a project by using ProjectConfig:

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

References