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)
Android bot looking through telescope

If you’ve spent some time testing Coroutines this exception should look familiar:

After waiting for 1m,
the test coroutine is not completing,
there were active child jobs

This tends to happen when you have a coroutine job in your test, that fails to complete on its own. Let’s take a simple example.

example code with channel
src on github

We use a Channel here which is the proverbial event bus for Coroutines.

Channel diagram
source: kotlinlang.org

Channels don’t terminate on their own. So when you run a simple test checking the emission, while the items might get collected correctly per the assert statement in the test, the test itself fails like so:

example code with channel
test on github

The test here is waiting for that coroutine job to complete, which in turn requires the Channel to, but that never happens and the test times out.

Where else would I run into this problem?

Channels aren’t the only case you’ll run into this problem. For example if you use a “hot” Flow like SharedFlow or StateFlow, they don’t terminate on their own, so the onus is on you to complete or cancel their Job in tests.

Android developers can frequently run into this problem too if you use ViewModels and have an internal StateFlow providing your “view level data” (what i personally like to call “view state”).

You typically use the viewModelScope to launch internal coroutine jobs in a ViewModel. The OS then calls the lifecycle method onClear when the Activity or Fragment no longer needs the ViewModel where all jobs started in the viewModelScope are canceled.

But when unit testing these ViewModels, you don’t have access to the viewModelScope and shouldn’t need it anyway.

Solving this problem

There’s two ways to solve this problem:

1. Manually cancel the Job

If you have access to the Job that spawns the never-ending coroutine, it’s trivial to cancel the Job. Here’s that first test, now passing:

fixed test - manually cancel job

A simple job.cancel does the trick.

2. Use a backgroundScope

But you don’t always have access to the Job directly or it requires a level of pass-through that makes your code cumbersome.

That’s where backgroundScope comes in and is a pretty elegant solution to this problem.

fixed test - using backgroundScope

Instead of getting access to the job directly, launch the desired coroutines within a backgroundScope.

backgroundScope is a special construct provided in coroutine tests alone that makes sure all child coroutines are explicitly canceled once the test body completes.

How does backgroundScope work?

The beauty of Kotlin is everything is open source. Let’s go find the code that that powers this magic:

The relevant code can be found as part of runTest in TestBuilders.kt.Here’s the interesting parts of the implementation:

public fun TestScope.runTest(
    timeout: Duration = DEFAULT_TIMEOUT.getOrThrow(),
    testBody: suspend TestScope.() -> Unit
) { scope ->
    // ...

    createTestResult {
        val testBodyFinished = AtomicBoolean(false)

         scope.start(CoroutineStart.UNDISPATCHED, scope) {
            yield()
            try {
                testBody()
            } finally {
                testBodyFinished.value = true
            }
        }
        // ...
        try {
            withTimeout(timeout) {
                // ...
                testBodyFinished.value && activeChildren.isNotEmpty() ->
                     "there were active child jobs: $activeChildren. " // ...
                testBodyFinished.value ->
                    "the test completed, but only after the timeout"
                else -> "the test body did not run to completion"
            }
        } catch (_: TimeoutCancellationException) {
            // ...
        } finally {
            backgroundScope.cancel()
            // ...
        }
    }
}

Make backgroundScope a regular part of your testing arsenal. In most of my tests these days, I liberally use and inject backgroundScope to make sure those tests don’t hang.

Next up: Controlling time