Testing Coroutines — Update 1.6.0

Ralf Stuckert
4 min readJan 3, 2022

--

About two years ago I wrote some articles describing the Kotlin Coroutines Testing module. With the recent 1.6.0 release things changed significantly, so it’s time for an update.

Photo by Ricardo Gomez Angel on Unsplash

Building Blocks

The building blocks of the kotlinx-coroutines-test module have changed a bit. You may still use the old API, but those functions are deprecated and subject to vanish in a future release. The TestCoroutineExceptionHandler has been dropped without replacement; pause/resume dispatcher has been replaced by dedicated dispatcher implementations, and virtual time control has been moved to a central scheduler. If you have to port your existing code to the new API, I recommend reading the migration steps. But let’s take this step by step.

TestCoroutineScheduler

The scheduler controls the execution order of coroutines and implements the concept of virtual time used for skipping delays. This is a major change to the previous version of the API, where the virtual time was part of the (now deprecated) TestCoroutineDispatcher. The scheduler may be shared by multiple TestDispatchers, so execution order and time control is based on one central entity.

TestDispatcher

The actual dispatcher used to execute coroutines in tests. It is based on a scheduler which controls time and execution order. You may pass an existing scheduler, otherwise a new one is created. You may now use multiple TestDispatchers in a test, but be aware that they must all share one scheduler.

There are two implementations of the TestDispatcher: the StandardTestDispatcher and the UnconfinedTestDispatcher. The StandardTestDispatcher does not run child coroutines right away (FKA paused), where the UnconfinedTestDispatcher executes them eagerly.

TestScope

A CoroutinesScope that integrates with the other building blocks and provides access to the TestCoroutineScheduler.

runTest

This function replaces runBlockingTest and runs a code block providing a TestScope, automatically skipping delays (via the TestCoroutineScheduler), and handles uncaught exceptions.

Multiplatform Support

kotlinx-coroutines-test became a multiplatform library in version 1.6.0, and is usable from Kotlin/JVM, Kotlin/JS, and Kotlin/Native. The only requirement is that you immediately return the result of runTest():

The reason for that is the return type runTest(...): TestResult . On JVM the actual implementation is Unit, but on Kotlin/JS it is a Promise that must be handled by the test framework. So if you want it to work on all platforms, make sure to return the TestResult immediately.

Standard- and UnconfinedDispatcher

In the old API child coroutines have been executed eagerly until the first suspension point. If this behavior was unwanted, you could change it by pausing and resuming the dispatcher. This was meant to be comfortable, but was not quite intentional in some cases. In order to provide clean semantics, this has been changed by providing two separate TestDispatcher implementations. By default, runTest() provides a StandardTestDispatcher, which does not execute child coroutines right away, let’s see this by example:

So in order to execute the child coroutine, you either have to join the job, yield execution (to the child coroutine), or use runCurrent() or advanceUntilIdle() which triggers the scheduler to execute pending coroutines.

This makes the execution order quite explicit and comprehensible, and that’s why it is used by default. If this behavior is not desired, you may use an UnconfinedTestDispatcher as an alternative:

That’s a lot shorter. But be aware, that eager execution stops at the first suspension point in the child coroutine. Also the unconfined dispatcher won’t help you much, if the child coroutine is started lazily:

Behavior of advancing time changed

Time control has been moved from the test dispatcher to the TestCoroutineScheduler. From a usage perspective, things do not seem to have changed at all:

So the TestScope still provides the property currentTime and the functions advanceTimeBy(), advanceUntilIdle() and runCurrent() for you, but instead of delegating to the dispatcher, they now delegate to the TestCoroutineScheduler , and here comes the catch: the behavior of advancing time in the scheduler is slightly different now:

Before version 1.6.0, advanceTimBy(n) would have run a task scheduled at currentTime + n, but now it will stop just before executing any task starting at the next millisecond. So now you have to either call runCurrent() or advance time by another ms. This means if we had called advanceTimeBy(1001), the task would have been executed right away.

Sharing the Scheduler with multiple Dispatchers

Extracting time and execution control to some central scheduler entity has its benefits: you can now use multiple test dispatchers that share the scheduler, which allow you to control execution order. So you may now use even different dispatchers like e.g. both Standard and Unconfined:

starting
1
unconfined is already started, runCurrent() will start standard
2
advanceTimeBy(n) will not run task scheduled at currentTime + n …
… but runCurrent() will:
3
runTest() will auto-advance all pending tasks at the end
4

Main Dispatcher

For dealing with UI frameworks there is a Main dispatcher which usually works using a dedicated UI-Thread. In order to write tests for UI code, there was already some support by using Dispatchers.setMain(testDispatcher) (for more information read my previous article on that topic). The pain with that solution was, that you had to use the dispatcher from the test scope, which proved to be inconvenient.

This has been eased up a lot: the TestDispatcher implementations (both standard and unconfined) now check if there is a TestMainDispatcher already in charge, and — if so — share the existing scheduler:

So you may now easily set up your own Main TestDispatcher first. runTest() will create its own ( Standard) TestDispatcher , but it is using the existing scheduler from the TestMainDispatcher. This strips the pattern for tests involving the Main dispatcher down to this:

So this was a brief description of the changes in the kotlinx-coroutines-test1.6.0 release. Over the next few weeks I will update the existing articles and examples in order to provide some more details on the new API, stay tuned.

Because things are the way they are,
things will not stay the way they are.

Bertolt Brecht

--

--