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)
Coroutine Testing - Tardis

My journey with coroutine testing started with this “simple” requirement — to control virtual time in concurrent logic.

From my previous post:

… If you don’t use a StandardTestDispatcher explicitly, then operators like runCurrent, advanceTimeBy etc. have no meaning.

I have a confession to make. I was coy saying “have no meaning”. I didn’t say “won’t work” because in reality those apis will “work” just not in the way you’d expect.

Let’s dive deeper.

The 3 apis

There are 3 important apis in coroutine tests to play with time:

  1. runCurrent - execute tasks scheduled at the current moment of virtual time
  2. advanceTimeBy - advance virtual time by a number of milliseconds and then execute tasks scheduled in the meantime.
  3. advanceUntilIdle - similar to advanceTimeBy but instead of advancing by a specific number of milliseconds, it keeps advancing until no more scheduled tasks are found.

In the context of coroutine testing, when you think of “time”, it refers simply to the order that the TestCoroutineScheduler decides to execute your scheduled coroutines. That’s why the definitions above stress on words “execute” vs “scheduled”.

In reality, these apis belong to the class TestCoroutineScheduler (not StandardTestDispatcher as the coy comment might have indicated).

Let’s attempt to understand this all, with some code.

StandardTestDispatcher & the 3 apis

This animation should illustrate how the different apis are designed to work1.

StandardTestDispatcher is good about respecting the 3 apis.

A basic test demonstrating the use of runCurrent that shouldn’t be surprising:

@Test
fun test() = runTest(StandardTestDispatcher()) {
	var result = "X"

	launch {
	  result = "A"
	  delay(1.seconds)
	  result = "B"
	  result = "C"
	  delay(2.seconds)
	  result = "D"
	}

	assertThat(result)
         .isEqualTo("X")

	runCurrent()
	assertThat(result)
         .isEqualTo("A")
}

Before runCurrent the coroutine isn’t launched so we expect the result “X”. On runCurrent we execute any scheduled coroutines without advancing the virtual clock, so expected result emitted2 is A.

Let’s advanceTimeBy exactly 1 second (the first delay):

// ...
launch {
  result = "A"
  delay(1.seconds)
  result = "B"
  result = "C"
  delay(2.seconds)
  result = "D"
}

advanceTimeBy(1.seconds)
assertThat(result)
        .isEqualTo("A")
        .isNotEqualTo("B")
        .isNotEqualTo("C")

Notice that even though we advanced time by the delay, the result hasn’t updated or changed from A. This is because you’ve scheduled the next emit with the delay but haven’t executed it yet.

Throw in a runCurrent like before and it’ll proceed to execute, giving you the result you expect:

// ...
launch {
  result = "A"
  delay(1.seconds)
  result = "B"
  result = "C"
  delay(2.seconds)
  result = "D"
}

advanceTimeBy(1.seconds)
assertThat(result)
        .isEqualTo("A")
        .isNotEqualTo("B")
        .isNotEqualTo("C")

runCurrent()
assertThat(result)
        .isEqualTo("C")

To really test your understanding - what happens if you advance time by 2 seconds (greater than the first delay of 1 second but less than the next one of 2 more seconds):

// ...
launch {
  result = "A"
  delay(1.seconds)
  result = "B"
  result = "C"
  delay(2.seconds)
  result = "D"
}

advanceTimeBy(2.seconds)
assertThat(result)
         .isEqualTo("C")

Advancing it by 2 seconds means you’ve gone past the initial one second delay (where the job is scheduled) + an additional 1 second, nudges the job to execute (releasing B & C). We don’t need a runCurrent here like the previous example because you’ve nudged the virtual clock past the scheduled time, well into execution. This demonstrates how going past the scheduled delay -even by a small amount- will automatically execute on the results.

This entire test should now make sense:

@Test
fun test() = runTest(StandardTestDispatcher()) {
  var result = "X"

  launch {
    result = "A"
    delay(1.seconds)
    result = "B"
    result = "C"
    delay(2.seconds)
    result = "D"
  }

  advanceTimeBy(1.seconds)
          // result A executed
          // schedule B & C
  assertThat(result)
           .isEqualTo("A")

  runCurrent()
          // execute B & C
  assertThat(result)
	         .isEqualTo("C")

  runCurrent()
          // no-op (no time advancement)
  assertThat(result)
            .isEqualTo("C")

  advanceTimeBy(2.seconds)
         // schedule D
  assertThat(result)
            .isEqualTo("C")

  runCurrent()
           // execute D
  assertThat(result)
            .isEqualTo("D")

  // 😉
}

The key point to take away with StandardTestDispatcher is that it respects the apis but you have to really understand the difference between scheduling a job and executing it.

UnconfinedTestDispatcher & the 3 apis

This is where things get complicated. Don’t take my word for it , even the docs say so:

“Unusual” is correct.

I want to first point you to some source code that will help build the necessary context. If you look at the implementation for both TestDispatchers, you’ll see that they both utilize a TestCoroutineScheduler:

StandardTestDispatcher src
UnconfinedTestDispatcher src

Remember TestCoroutineScheduler is effectively your virtual clock and is the actual class that provides the 3 big apis. So it follows that UnconfinedTestDispatcher has some effect from the apis.

There’s two methods of interest isDispatchNeeded and dispatch.

isDispatchNeeded from CoroutineDispatcher

Looking at the implementation for the method dispatch in both TestDispatcher classes. You can piece together some things:

// UnconfinedTestDispatcher

override fun isDispatchNeeded(context: CoroutineContext): Boolean = false

override fun dispatch(context: CoroutineContext, block: Runnable) {
    checkSchedulerInContext(scheduler, context)
    scheduler.sendDispatchEvent(context)
    //...
}
//  StandardTestDispatcher

// fun isDispatchNeeded not overridden so defaults to true

override fun dispatch(context: CoroutineContext, block: Runnable) {
    scheduler.registerEvent(this, 0, block, context) { false }
}

UnconfinedTestDispatcher sends the dispatch event right away. It further overrides isDispatchNeeded as false meaning - execute the coroutine immediately, don’t first register (or schedule) the coroutine just send or dispatch.

StandardTestDispatcher on the other hand is “simpler” and merely registers (or schedules) the event to be dispatched. When does it get executed or dispatched then if right away? That’s where the 3 apis come in 💡. This is why it feels like the 3 apis to advance time have more meaning with StandardTestDispatcher.

Let’s look at test code for UnconfinedTestDispatcher similar to our StandardTestDispatcher examples. This is a super basic test for UnconfinedTestDispatcher.

@Test
fun test() = runTest(UnconfinedTestDispatcher()) {
  var result = "X"

  launch {
    result = "A"
    delay(1.seconds)
    result = "B"
    result = "C"
    delay(2.seconds)
    result = "D"
  }

  assertThat(result).isEqualTo("A")
}

Already, we start with some unusual-ness. Notice that we didn’t runCurrent but the very first value “A” was eagerly emitted 3. In the case of StandardTestDispatcher, the value would have been “X”.

The docs actually point this behavior out:

* Like [Dispatchers.Unconfined], this one does not provide guarantees about the execution order when several coroutines
* are queued in this dispatcher. However, we ensure that the [launch] and [async] blocks at the top level of [runTest]
* are entered eagerly. This allows launching child coroutines and not calling [runCurrent] for them to start executing.

The funny thing is UnconfinedTestDispatcher will actually behave very similarly to StandardTestDispatcher now in terms of advancing time (after it is eagerly launched):

launch {
  result = "A"
  delay(1.seconds)
  result = "B"
  result = "C"
  delay(2.seconds)
  result = "D"
}

assertThat(result).isEqualTo("A")

// now it'll behave like StandardTestDispatcher
advanceTimeBy(1.seconds)
runCurrent()

assertThat(result).isEqualTo("C")
advanceTimeBy(2.seconds.plus(1.milliseconds))
assertThat(result).isEqualTo("D")

So it does respect the 3 apis but only after eagerly launching. Hopefully that clarifies the original comment.

To be honest I’ve asked myself many times what the point of having both kinds of TestDispatchers is, if this remains the only big difference. One important reason is that seismic change in apis with 1.6.0 — older testing apis like runBlockingTest (now deprecated) used to launch coroutines eagerly to make testing less tedious, so it was an easier migration path to swap in place with UnconfinedTestDispatchers.

Here’s my takeaway between the two dispatchers:

  1. UnconfinedTestDispatcher launches the coroutine eagerly (explained above) which makes it less tedious during testing
  2. Order is not guaranteed with UnconfinedTestDispatcher, so StandardTestDispatcher is more predictable especially in complicated concurrent scenarios
  3. UnconfinedTestDispatcher is good about collecting every single item (this doesn’t have to do with time) but maybe in a future post I’ll show how testing StateFlows can benefit from UnconfinedTestDispatcher.

Just as manual and automatic transmission cars offer different levels of control and convenience in driving, the UnconfinedTestDispatcher and StandardTestDispatcher in Kotlin coroutines provide distinct approaches for managing the execution of test code.

I personally do prefer using StandardTestDispatcher because I’m not one to shy away from boilerplate, so I happily trade off tediousness with predictability but your mileage might vary.

My thanks to Márton for reviewing this post. Next up: A Helpful @Junit CoroutineTestRule extension (coming soon)


  1. I spent way too much time making this animation, so I’m dropping it right at the start. Though if I’m being honest, it’s faster to look at the following code and understand. ↩︎

  2. I’m using the terminology emit because the animation was meant to illustrate a MutableStateFlow. For the tests shown here, think of emit as the result being assigned a value. ↩︎

  3. Again I’m using the terminology emit as before, but think of emit as the result being assigned a value. ↩︎