Migrating to the new coroutines 1.6 test APIs
kotlinx.coroutines 1.6 introduces a set of new testing APIs, and the previous testing APIs are now deprecated. Using the old APIs will produce deprecation errors soon, and they’re scheduled to be removed completely around the end of 2022.
We have recently published a guide on how to use the new testing APIs, which explains how they work in detail. In this post, we’ll focus on the migration from the old APIs by looking at how we’ve migrated some of our own samples. You can find links to view the full diffs at the end of this post.
The migration steps we took should cover a lot of the necessary work for most Android projects. If you find that these are not enough for your project, you can take a look at the detailed migration guide by JetBrains which covers advanced usages of the testing APIs as well.
Start with runTest
Let’s start at the entry point of the new testing APIs, the
runTest coroutine builder. This replaces
runBlockingTest from the old APIs, which could be called as a top-level function, but it was also often invoked on a test scope, test dispatcher, or test rule.
We’ve replaced all of these with calls to the top-level
If you weren’t using an expression body (directly returning
runTest’s result from the function) yet, it’s a great time to adopt that convention, too! It’s nice for consistency, and it’s required if you’ll ever use the coroutine testing APIs in a multiplatform project with a KotlinJS target.
Handle the Main thread
As the Android UI thread is not available in unit tests, any tests relying on the
Main dispatcher need to replace it with a
TestDispatcher implementation for the duration of the test. You can either inject it like other dispatchers, or replace it using
setMain replaces the dispatcher in a static way, which means that you can use constructs that rely on a hardcoded
Main dispatcher in your tests, such as
A frequently used method for this is to put the code replacing
Main into a reusable JUnit test rule (or for JUnit 5, a test extension). You can see an example of such a rule in the iosched project. If you have a rule like this using the old APIs, update it like this:
The rule is then used like this, as a property of the test class (unchanged from before):
Be more eager: collecting Flows
Tests often start new coroutines to collect values from Flows. These tests tend to rely on these new coroutines being started eagerly, so that whenever the Flow emits a value the collector will already be ready to process it.
runBlockingTest starts new coroutines created within the test eagerly,
runTest starts them lazily instead, as it uses a
StandardTestDispatcher for the test coroutine by default.
To make Flow-collecting coroutines in tests start eagerly again, create a new
UnconfinedTestDispatcher, and pass it to the builder that creates the collecting coroutine:
Note that in this code snippet, a new
TestDispatcher is created without passing in a scheduler explicitly. This is safe to do only if the
Main dispatcher has been replaced by a
TestDispatcher, which makes scheduler sharing automatic. Otherwise, you have to pass in the existing scheduler to any
TestDispatchers you create:
It’s also worth remembering that calling collect explicitly is not the only way to collect a Flow, other methods like
Flow.toList() also collect the Flow internally. If you’re using such methods, you might also want to call them in new coroutines that are started with an
Be less eager: Main dispatcher execution
As you’ve seen above in the implementation of
MainDispatcherRule, we default to using
UnconfinedTestDispatcher for the
Main dispatcher to eagerly launch coroutines. This is useful when testing ViewModels, where using
Dispatchers.Main.immediatewould have similar eager behavior in production code when called from the main thread.
However, some tests in our samples needed lazy scheduling for
Main dispatcher coroutines. Typically, this would be for tests that need to assert an intermediate loading state of a ViewModel, where eagerly starting the data-loading coroutine would mean that the test can only observe the final loaded state. With the old APIs, these tests used
pauseDispatcher to prevent new coroutines from executing too early, like this:
To perform the same test with the new APIs, the
Main dispatcher needs to be set to a
StandardTestDispatcher, so we need a different
TestDispatcher type than what our rule uses by default. As the type of
TestDispatcher used for
MainDispatcherRule affects all tests within the test class, we had two choices:
- split tests into two test classes based on the type of
Maindispatcher needed for each test, using a rule with a different type of dispatcher in the two test classes, or
- keep using a single class where the rule always sets an
Main, and then override the
Maindispatcher’s type in just a few of the tests that require a different type.
We opted for the latter solution, starting these tests by replacing the already-replaced
Main with a new
StandardTestDispatcher to lazily start new coroutines on
Main. Then, later in the test code when we’d call
resumeDispatcher with the old APIs, we can advance those coroutines by using
This approach keeps tests that belong together in the same test class, making the codebase easier to navigate, with the tradeoff that some tests have to include extra code for replacing
Main with the desired type of
Clean up that cleanup code
Finally, some quick tidying-up to do. The iosched sample had some test code that explicitly waited for coroutines to complete on the
TestCoroutineDispatcher before the test would end:
runTest automatically waits for all known coroutines, which include children of the test coroutine and any coroutines running on
TestDispatchers. This means that you can just remove any cleanup code that waits for some loose coroutines to complete!
These migration steps should get you most of the way toward using the new coroutine testing APIs. For more, you can check out all the changes we made in our samples:
And of course, the new Now in Android sample app already uses the new testing APIs for its tests!
Finally, if you need more help with the migration, check out the official migration guide by JetBrains, which covers the intricacies of the coroutine testing API.