Testing Coroutines — Timeout
The first part of this series provided you an introduction to the building blocks of the kotlinx-coroutines-test module, and gave you some examples on eager execution and virtual time. In this second part we will have a look at testing timeouts.
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.
We want a software system to feel responsive to the user, meaning a system should respond in a given time, either with the result or a failure. Since systems usually interact with other systems — be it a server, a database or whatever — we do not want to wait forever for a response. A common technique to solve this problem is using timeouts: If the operation does not respond in a given time, we will raise a timeout failure.
Kotlin coroutines already provides a function withTimeout()
that helps you implement that. Let's do this by example:
We have UserService
that loads User
from a remote system, so this operation might take a while. This service is injected into the
suspendable loadUser()
function which wraps the access to the service using withTimeout()
.
Now we will write some test that will check our timeout behaviour. Let us start with the trivial success case, where the service responds immediately. In order to mock the UserService
we will use the MockK library (if you haven't used it yet, give it a try):
So what are we doing here: we create a mock for the UserService
and set it up to return our prebuilt user on every load call (coEvery
and coAnswer
are variants meant to deal with suspendable functions). Then we just check if the returned user is the one we set up in our mock.
Now let us write a test that checks that loadUser()
fails after the given timeout. This is quite easy due to the virtual time implemented by the TestCoroutineDispatcher
which gives us explicit control:
So what did we do here? We simply let our mock delay for 30 seconds. In this case we expect a TimeoutCancellationException
to occur. As already mentioned in the first part, virtual time is reliable. So we can reliably test the boundaries of our timeout by expecting success with a delay of 29.999ms:
By now our function under test was suspendable. Now let’s deal with a function that starts a new coroutine using async
:
Testing this is straight forward like we did before. Just call await
to complete the deferred:
If you’re wondering where this coAssertThrows()
function comes from: this is a custom variant of the standard assertThrows()
that is capable of dealing with suspendend functions. Alas, suspendable functions differs from regular functions, read Roman Elisarov's explanation on why the suspend modifier colors functions in Kotlin.
That’s it so far, the next part will be on dealing with dispatchers. You will find all examples in the accompanying git repository.
Why do we never have time to do it right,
but always have time to do it over?
Murphy’s Law