Migrating to the new coroutines 1.6 test APIs

Marton Braun
Android Developers
Published in
5 min readJun 29, 2022

--

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 runTest function:

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.

In some advanced cases, you might still want to create your own TestScope, but most tests will need only to call runTest on its own.

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 Dispatchers.setMain. 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 viewModelScope.

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.

While 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 UnconfinedTestDispatcher.

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 Main dispatcher 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 UnconfinedTestDispatcher for Main, and then override the Main dispatcher’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 TestDispatcher in 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 advanceUntilIdle.

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 TestDispatcher.

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:

However, 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!

Wrap-up

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.

--

--

Marton Braun
Android Developers

Kotliner, Android DevRel @ Google, Instructor @ BME-VIK.