Testing Coroutines — Update 1.6.0
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.
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.
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.
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
StandardTestDispatcher and the
StandardTestDispatcher does not run child coroutines right away (FKA paused), where the
UnconfinedTestDispatcher executes them eagerly.
A CoroutinesScope that integrates with the other building blocks and provides access to the
This function replaces
runBlockingTest and runs a code block providing a
TestScope, automatically skipping delays (via the
TestCoroutineScheduler), and handles uncaught exceptions.
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
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
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
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
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:
TestScope still provides the property
currentTime and the functions
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
unconfined is already started, runCurrent() will start standard
advanceTimeBy(n) will not run task scheduled at currentTime + n …
… but runCurrent() will:
runTest() will auto-advance all pending tasks at the end
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
runTest() will create its own (
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.