Testing Coroutines — Remainder

Ralf Stuckert
3 min readApr 2, 2020

--

Photo by Kotagauni Srinivas on Unsplash

In the last part of the series on the testing coroutines with the kotlinx-coroutines-test module, we will have a look at remaining topics like exception handling, pausing the dispatcher and some more stuff.

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.

Exception Handling

If we want to test for exceptions in coroutines we could do so using the good old runBlocking():

This works since runBlocking() rethrows the first occurring exception if no one cares for it. An alternative is to provide a dedicated CoroutineExceptionHandler in the context, which will be called on every rising exception. The runBlockingTest() function adds a TestCoroutineExceptionHandler to the context to do so. This handler also throws the first occurring exception on cleanup, but also allows to inspect all exceptions during the test:

So this might be quite helpful in some cases. But it also may have some unexpected effects you should be aware of. A SupervisorScope is usually used if you do NOT want the supervising scope to fail, if one of the children fails. So let's see the following example, would you expect it to succeed?

Nope, it fails. This is due the exception handler provided by runBlockingTest() which is passed to the supervisor scope. It handles all exceptions and rethrows it. If you use runBlocking() it would run right through.

So what if I do want to make use of all those goodies of runBlockingTest() without having a handler throwing exceptions? You could provide a custom handler that either does nothing at all, or one that just collects all exceptions but does not rethrow. This has the benefit that you can still analyze exceptions in your test:

Pausing the Dispatcher

As explained in the first part, the TestCoroutineDispatcher executes new coroutines immediately like they where started in mode UNDISPATCHED. This behaviour can be changed by pausing the dispatcher, see the following example. Be aware that these example do not make any sense and are solely intended to demonstrate order of execution:

Due to the call to pauseDispatcher(), the launched coroutine is NOT executed immediately, that's why state is still 0. Calling runCurrent() makes the dispatcher forward all coroutines up the current virtual time, so it proceeds up to the delay(1000) and therefore state == 2. To proceed we could either advance time (by 1000), or advanceUntilIdle() to run right through.

To complement pauseDispatcher() there is also a resumeDispatcher(). This switches execution back to immediate mode, but also advances all coroutines until idle. See the following example:

To complete this, there is also a pauseDispatcher()variant that executes a given block of code. After the block is executed, it also resumes the dispatcher automatically:

Detecting Unrelated Jobs

The TestCoroutineScope resp. runBlockingTest() is quite dependent on the TestCorouineDispatcher to make time control work properly. But it has some built in feature in order to detect misusage. Let's recap our dispatcher provider example from part 3. We have a function loadUserIOthat uses the io dispatcher from the DispatcherProvider

… and the following test:

If you run this test, you will get:

java.lang.IllegalStateException: This job has not completed yet

Why is that? Ooops, we accidently used runBlockingTest() instead ofrunBlockingTestProvided(). So the test dispatcher will not be passed through the provider, but the original Dispatcher.IO is used. If we run the test, it will automatically advance until idle. But since we are not using the test dispatcher, it will not advance our coroutine :-(

Luckily runBlockingTest() has a safety belt: it counts all jobs before and after test execution. If there is a difference, it complains with the exception you saw above, and therefore gives you hint that something is wrong with your test :-)

That’s it, we’re done. There are quite a lot things you can do with the kotlinx-coroutines-test module, so give it a try. Be aware that the API is still experimental, so things are subject to change. You will find all examples and the utility functions in the accompanying git repository.

You’re damned if you do and
you’re damned if you don’t.
Bart Simpson

--

--