Testing Coroutines — Introduction
This is the first part of my little series on testing coroutines, where we will explore the capabilities of the kotlinx-coroutines-test module. All examples (and some utility functions) are provided in a Git repository, so give it a try. This first part will introduce the building blocks of the module, and give some examples on eager execution and virtual time.
Update: This article describes the old API. With 1.6.0 significant changes has been introduced which are described here. Over the next few weeks I will update this article and the examples in order to provide some more details on the new API, stay tuned.
Building Blocks
The kotlinx-coroutines-test module consists of four ingredients: The TestCoroutineDispatcher
, a TestCoroutineExceptionHandler
, the TestCoroutineScope
and finally the runBlockingTest()
function. Let's take them step by step.
TestCoroutineDispatcher
In contrast to other dispatchers, this one executes new coroutines immediately like they where started in mode UNDISPATCHED
. This eases the handling in tests a bit. But you can change this behaviour, either by providing a dedicated start mode, or by pausing the dispatchers.
Another important point is, that it is up to a dispatcher to implement the concept of time on which scheduling and delay is based on. The TestCoroutineDispatcher
implements a virtual time and gives you fine grained control on it. This allows to write robust time based tests.
TestCoroutineExceptionHandler
A CoroutineContext
may contain a CoroutineExceptionHandler
which is comparable to the uncaught exception handler on threads, and is intended to handle all exceptions that arise in a coroutine. This implementation captures and collects all exceptions, so they can be inspected in tests, and rethrows the first one on cleanup.
TestCoroutineScope
This scope provides a TestCoroutineDispatcher
and TestCoroutineExceptionHandler
by default, if none is already given in the context. It also provides access to the the time controlling functions like advanceTime...
and the uncaughtExceptions
by delegating them to the TestCoroutineDispatcher
resp. TestCoroutineExceptionHandler
.
runBlockingTest
This variant of runBlocking()
ties everything up and provides you a TestCoroutineScope
and therefore a TestCoroutineDispatcher
and TestCoroutineExceptionHandler
. It advances time of the test dispatcher until idle, which effectively makes sure that everything in the test block is run. It also checks for misusage by counting the active jobs before and after the test.
Eager Execution
As already told the TestCoroutineDispatcher
executes coroutines eagerly, means there is no need to e.g. join a job. For comparison let's first see the old way. Be aware that these example do not make any sense and are solely intended to demonstrate order of execution ;-)
Now the same scenario with a test dispatcher provided by runBlockingTest()
. No need to join, and therefore no need for the job reference. This makes it a bit more readable:
A remarkable thing here is that the launched coroutine will be executed immediately, but on suspension, e.g. by yield()
or delay()
, execution will resume after the launch block. Luckily the DelayController
implemented by the test dispatcher provides a function runCurrent()
that resumes the coroutine:
Advancing Time
Sometimes we have to deal with delays, be it in code or e.g. for testing timeout. Waiting for those delays in test can be tedious:
If you run this test, it will take a bit more than 5 seconds. Now let’s do the same thing with runBlockingTest()
It takes just some milliseconds to run. The reason for that is, that the TestCoroutineDispatcher
implements a virtual time, and runBlockingTest()
auto-advances time, so the test will run right through. You have even access to that virtual time:
Be aware that the eager execution of a new coroutine also suspends at a delay. As an alternative to runCurrent()
you can also use the advance...()
function family to forward the execution of the coroutine:
That means even if this test executes in a couple of millis, the virtual time is quite reliable, so you can depend on it in your tests:
I guess that’s enough for today, the next part will be on testing timeout. As already mentioned, you will find all examples in the accompanying git repository.
Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.
Martin Golding