Testing Coroutines — Dispatchers
In part one and two of this series we saw how to make use of the time control features of the kotlinx-coroutines-test module. In this part you will see how to test code that utilizes a dedicated dispatcher.
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.
Main Dispatcher
UI code like e.g. Android, Swing, JavaFX is executed by a dedicated UI-Thread. When using coroutines you have to use the Main dispatcher
for that purpose. But how do we test this code? We could integrate a dedicated platform main dispatcher in our tests, but that is not always feasible. Moreover, those neat time control features introduced in the first two parts would not be accessible. Luckily, the coroutine test module provides functionality to fill in this gap. To demonstrate this, we will introduce a function waitForUserConfirm()
that is supposed to be UI code:
In order to call it from anywhere in our code — where anywhere means: regardless of the current dispatcher — we have a function confirmDone()
that makes sure that the UI code is executed on the Main
dispatcher:
Now let us write some that is intended to test the confirmDone()
function:
We set up our UI mock to delay a bit (our user is not the fastest ;-) and return OK. Run the test: As already said, if we do not provide any main dispatcher this test will complain:
java.lang.IllegalStateException: Module with the Main dispatcher is missing.
Add dependency providing the Main dispatcher, e.g. 'kotlinx-coroutines-android'
and ensure it has the same version as 'kotlinx-coroutines-core'
To fix this, we will use the setMain()
function which allows to set our own dispatcher as main. In order to benefit from all goodies we will use the TestCoroutineDispatcher
provided by runBlockingTest()
:
Now our test becomes green. In case you are wondering where that testDispatcher
is coming from: this is a tiny custom helper function, hopefully there will something "official" soon, see issue 1609.
Main Dispatcher JUnit Extension
To ease the handling of using a test dispatcher for main, you could also use a JUnit rule resp. extension. Here is a variant of a JUnit 5 extension that creates a TestCoroutineDispatcher
for each test, sets it as the main dispatcher, and provides it as a parameter for the test. You have to use this dispatcher either by passing it as a context to runBlockingTest()
, or call runBlockingTest()
on the dispatcher (which is effectively the same thing):
Other Dispatchers
So this works well for the Main
dispatcher. What about the other ones, like e.g. Default
or IO
? Let's do this by example: we will take our already known UserService
that loads a user from e.g. a database. In order to make sure that this is done via IO
dispatcher, we have a functionloadUserIO()
:
We could easily write a test like we did before, but how can we inject our test dispatcher? Is there a setIO()
function? Nope. Maybe there will be one in future releases (see issue 1365), but not at the time of this writing.
A common approach is to inject a DispatcherProvider
, an interface that abstracts the direct usage of dispatchers. To do so, we have to change our loadUserIO()
as well:
Now for production code, we have to use a provider that utilizes the real dispatchers, but we can inject the test dispatcher in our unit tests:
So this TestDispatcherProvider
implements the DispatcherProvider
interface by always providing the given (test-) dispatcher for main
, io
, etc. Since this idea is widespread, there are already some libraries out there doing the job. I'm using this neat library here. Besides providing a production and test dispatcher provider, it can implicitly provide the DispatcherProvider
in the coroutine context, and has some extension functions to access it. The advantage is, that you do not have to alter your signature in order to pass the provider, which eases migration from direct usage of the Dispatchers
:
But how is the provider injected into the coroutine context? For production code you don’t have to do anything. If you access the provider, and there is none yet, the lib will instantiate the default provider that simply provides the orginal dispatchers, like you were using e.g. Dispatchers.IO
directly. For testing it has the convenience function runBlockingTestProvided()
: an extension of the original runBlockingTest()
which automatically adds the TestDispatcherProvider
to the context of the test scope:
That’s it for today, the last part will be on pausing the dispatcher and handling exceptions. As already mentioned, you will find all example in the accompanying git repository.
Being a Dispatcher means:
you will cry a lot, you will laugh a lot.
Anonymous