This is part of a series of posts on Coroutine Testing:

  1. Picking the right Dispatcher
  2. Never ending tests & backgroundscope
  3. Controlling time
  4. Helpful @Junit TestRule extension (coming soon)
  5. Full USF example for Android (coming soon)

Most of the problems and flakiness around coroutine testing stem from running them on different Dispatchers. This is because the choice of Dispatcher can significantly impact the behavior of coroutines. This was also the most confusing1 part for me starting out — understanding the implications of using a Scope, Context or Dispatcher.

I recommend Roman’s article if you want to brush up on the fundamentals. But in a nutshell: think of CoroutineContext as a collection of elements that define the coroutine. It contains a Dispatcher, Job & a CoroutineName. When you launch a coroutine, it inherits the parent’s CoroutineContext (and Dispatcher), unless you specify it explicitly.

A CoroutineScope on the other hand is just a way to manage and cancel (multiple) coroutines. It also defines a context and lifecycle for the coroutines launched within it (the context could be linked to yet another Dispatcher). Any coroutine when launched, runs within a CoroutineScope.

Let’s take an example:

Notice how the current Dispatcher of the coroutine shifts from StandardTestDispatcherUnconfinedTestDispatcherDispatcher.IO in the span of three innocuous lines based on the coroutine builder (runTest) or scope used (TestScope, turbineScope from the 3rd party library, App scope).

In my initial post I pointed out this flaky test:

flaky test code on github

The fix for this is as simple as explicitly injecting a TestScope and making sure the same scope is used throughout.

Flaky test fixed
fixed test code on github

Explicitly injecting the CoroutineScope and substituting it with the TestScope works really well and is my preferred strategy. This approach allows for more control over the Dispatcher used in tests . For reasons you’ll see later, these tests also run instantly (72ms vs 6s 82ms).

But the story doesn’t end there.

If you look at the official android docs, they recommend using the same Scheduler. Not a Scope or Dispatcher.

developer.android.com

What is a Scheduler?

This got me thinking… what Scheduler do my tests use? Easy enough to check:

StandardTestDispatcher[
   scheduler=TestCoroutineScheduler@1623134f
]

StandardTestDispatcher & TestCoroutineScheduler - the plot thickens. What even is a Scheduler? Rx had this concept but with Coroutines, Dispatchers is the only thing we deal with.

Let’s dive deeper.

Coroutine test parts
Courtesy: Márton's talk on Coroutine testing

runTest is the basic api we use for coroutine testing and like other coroutine builders (runBlocking , launch, async etc.) implicitly uses a CoroutineScope. For testing, there is a special scope called TestScope. And given that CoroutineScopes get linked to a Dispatcher2, for this special scope there exists a special TestDispatcher (more on this later).

When testing coroutines, we often need precise control over their execution timing, especially when dealing with virtual time. TestCoroutineScheduler is the construct that provides us this fine-grained control. This is also why you don’t see or use Schedulers in production or your app code, where there isn’t this need.

In a future post, I’ll show you a @Junit test rule that makes sure the TestCoroutineScheduler is shared across injected dispatchers.

But before we get into that, let’s look at that official android doc again:

developer.android.com

Any number of TestDispatchers? How many are there?

TestDispatchers

In testing, instead of using one of the regular Dispatchers (Default, IO, Main, Unconfined) you need to use a special TestDispatcher. Think of the TestCoroutineScheduler as the virtual clock and TestDispatchers as special Dispatchers. These special Dispatchers use the virtual clock to schedule and manage the execution of coroutines in tests.

TestDispatcher itself is an abstract class though and there exists two implementations that we can use - StandardTestDispatcher & UnconfinedTestDispatcher. Craig has an excellent post comparing the two but in a nutshell:

StandardTestDispatcher:

if not specified explicitly, runTest will use a StandardTestDispatcher by default

UnconfinedTestDispatcher:

Eager loading of coroutines in tests

The fact that StandardTestDispatcher does not execute tasks automatically lands up being a pretty important difference in the world of concurrency. UnconfinedTestDispatchers are more convenient to use but you trade-off on a deterministic test run.

You’ll observe that the extremely convenient coroutine Flow testing library Turbine uses an UnconfinedTestDispatcher internally4.

My complaint in the initial post around why advanceTimeBy wasn’t working, should now make sense. If you don’t use a StandardTestDispatcher explicitly, then operators like runCurrent, advanceTimeBy etc. have no meaning5. This is because coroutines are loaded eagerly, especially with vanilla Turbine test usage.

I strongly recommend using StandardTestDispatcher when you care about concurrency.

Phew! You’re now primed with the necessary fundamentals for Coroutine testing. The following posts should be targeted and a breeze.

My thanks to Márton & Robert for reviewing this post. Next up: Never ending tests & backgroundscope


  1. and arguably one of the ways coroutines is more complex than RxJava ↩︎

  2. In an earlier version of this post I mentioned CoroutineScopes contain a Dispatcher. This is strictly not true. It can for example have an EmptyCoroutineContext which is the object that usually contains the Dispatcher, but the EmptyCoroutineContext doesn’t specify a Dispatcher at all. When you launch though using a builder function, that function supplies the scope, since there’s nothing else to use. ↩︎

  3. There’s a nuance here again. Most places mention the tasks do not execute automatically. That’s strictly not true. StandardTestDispatcher itself executes them for example if you manually yield the test thread. But the point still stands in terms of observed behavior/functionality. ↩︎

  4. I can go into the nuances of Turbine in a future post. It’s more involved than a simple UnconfinedTestDispatcher replacement. ↩︎

  5. Make sure to read my post on controlling time for more on this. ↩︎