Chapters

Hide chapters

Kotlin Coroutines by Tutorials

Third Edition · Android 12 · Kotlin 1.6 · Android Studio Bumblebee

Section I: Introduction to Coroutines

Section 1: 9 chapters
Show chapters Hide chapters

13. Testing Coroutines
Written by Luka Kordić

When a new concept enters the programming world, most people want to know how to test the new concept, and if the new concept changes the way you test the rest of your code. Testing asynchronous code to make sure it runs and functions correctly is a good thing. Naturally, people started asking how do you test coroutines, when it’s such a different mechanism, compared to what used to be used in the JVM world.

The process of testing code is usually tied with writing Unit and Integration tests. This is code which you can run fast, debug, and use to confirm that a piece of software you’ve written is still working properly after you apply some changes to the code. Or rather that it doesn’t work, because you’ve changed it, and now you should update the tests to reflect all the remaining cases.

Unit tests run a single unit of code, which should be as small as possible - like a small function’s input and output. Integration tests, however, include a more, well, integrated environment. They usually test multiple classes working together. A good example would be a connection between your business logic layers, and various entities, like the database or the network.

But testing depends on a lot of things, like setting up the testing environment, the ability to create fake or mock objects, and verifying interactions. Let’s see how to do some of those things with coroutines.

Getting Started

To start with writing tests, you have to have a piece of code that you will test out! However, there is something known as TDD - Test Driven Development where tests are usually written first followed by the code. Open up this chapter’s folder, named testing-coroutines and find the starter project. Import the project, and you can explore the code and the project structure.

First, within the contextProvider folder, you have the CoroutineContextProvider, and its implementation. This is a vital part of the testing setup because you’ll use this to provide a test CoroutineContext, for your coroutines.

class CoroutineContextProviderImpl(
    private val context: CoroutineContext
) : CoroutineContextProvider {

  override fun context(): CoroutineContext = context
}

Next, the model package simply holds the User which you’ll fetch and display in code, and use to test if the code is working properly.

data class User(val id: String, val name: String)

Next, the presentation package holds a simple class to represent the business logic layer of the code. You’ll use MainPresenter.kt to imitate the fetching of a piece of data.

class MainPresenter {

  suspend fun getUser(userId: String): User {
    delay(1000)

    return User(userId, "Filip")
  }
}

Finally, you’ll pass the data you fetch to the view layer, which will then print it out.

class MainView(
    private val presenter: MainPresenter
) {

  var userData: User? = null

  fun fetchUserData() {
    GlobalScope.launch(Dispatchers.IO) {
      userData = presenter.getUser("101")
    }
  }

  fun printUserData() {
    println(userData)
  }
}

One last thing you have to add, to be able to test code and coroutines, are the Gradle dependencies. If you open up build.gradle, you’ll see these two lines of code:

    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test: $kotlin_coroutines_version"
    testImplementation 'junit:junit:4.13.2'

The former introduces helper functions and classes, specifically to test coroutines, and the latter gives you the ability to use the JUnit4 testing framework, for the JVM.

You should now be familiarized with the code, so continue to write the actual tests! :]

Writing Tests for Coroutines

If you’ve never written tests on the JVM, know that there’s a couple of different frameworks you can use and many different approaches to testing. For the sake of simplicity, you’ll write a simple, value-asserting test, using the JUnit4 framework for the JVM test suite.

The name states what the test will do - assert and compare some values. This is one of the most basic test types you could write. In the Project View, open the MainViewTest file under the test -> view package. This is where you will begin writing your tests. Next, add the following code:

package view

import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import presentation.MainPresenter
import kotlinx.coroutines.ExperimentalCoroutinesApi

@OptIn(ExperimentalCoroutinesApi::class)
class MainViewTest {

  private val mainPresenter by lazy { MainPresenter() }
  private val mainView by lazy { MainView(mainPresenter) }

  @Test
  fun testFetchUserData() {
    // todo add test code
  }
}

This is a basic example of what a test class should look like. You should have all the dependencies you need to be declared above, and set up, with the tests following after the declarations.

But your test is still empty. To make it do something, you have to fill it with the code you need to verify and the values you need to assert and check. You’ll do this with a slightly modified AAA approach. AAA stands for Arrange, Act and Assert. Let’s examine each of the steps:

  • Arrange - Prepare all the data and dependencies you need, before you execute your tests. A good example would be prefilling the database with some values you’re trying to fetch and compare.
  • Act - Call the functions which produce results or achieve some behavior, which you will test or compare later on.
  • Assert - Verify function calls, and if the code behavior is correct, or compare the received values with the expected results.

By doing so, you’re splitting the test code into three sections, making it more readable, and clear, as to what you’re testing and why.

Change the testFetchUserData code to the following:

@Test
fun testFetchUserData() {
  // initial state
  assertNull(mainView.userData)
  
  // updating the state
  mainView.fetchUserData()
    
  // checking the new state, and printing it out
  assertEquals("Filip", mainView.userData?.name)
  mainView.printUserData()
}

It’s fairly easy to follow the code, as not much is going on. You have to first assume the initial state within MainView is null, and then proceed to fetch the data. Once the data fetching finishes, you should check the state again, to see if the data matches what you returned in code. If that’s fine, you can print out the data and see the output in the test console.

Note: You’re using assertEqual and assertNull from the JUnit framework, but there are many other functions in the framework to compare values.

To run this test click on the green run button that shows up in the gutter next to your testFetchUserData method.

java.lang.AssertionError: 
Expected :Filip
Actual   :null
<Click to see difference>

You should see a similar output. If everything’s so straightforward, then why did the test fail? Well, it’s because of how coroutines are built internally, that the underlying code didn’t execute properly, to update the data. Since multi-threading is involved, and this is only a simple test environment, the runner doesn’t know how to launch coroutines as it would in a real-world app, and as such, you don’t get a result back. This, in turn, causes the assertion to fail, ultimately failing your entire test! But there is a way to mitigate this, by setting up the test environment to be coroutine-friendly.

Setting Up the Test Environment

The problem you’re facing when running the test is because of the way coroutines and test environments work internally. Because you’re hardcoding the MainPresenter and MainView calls to the GlobalScope and Dispatchers.IO, you’re losing the ability for the test JVM environment to adapt to coroutines.

To avoid this, you have to be explicit about your threading rules in tests, when testing coroutine-related code. There are a couple of small steps you have to take, to fully achieve this, so let’s start with the simplest - forcing coroutines to block.

Running the Tests as Blocking

One of the greatest benefits of coroutines is the ability to suspend instead of block. This proves to be a powerful mechanism, which allows for things like simple context switching and thread synchronization, parallelism and much more.

However, when dealing with unit tests, you don’t want the code to suspend. Because if it does, you’re effectively losing valuable time, which you could otherwise use for writing or running more tests! To avoid the suspension of code, and force the coroutines to be blocking, you have to wrap the test in runTest. It’s a special coroutine builder, which is built just for this occasion.

/**
 * Executes [testBody] as a test in a new coroutine, returning [TestResult].
 */
@ExperimentalCoroutinesApi
public fun runTest(
    context: CoroutineContext = EmptyCoroutineContext,
    dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
    testBody: suspend TestScope.() -> Unit
): TestResult

It’s still experimental, but that shouldn’t worry you too much. Even though it’s experimental, it is stable and ready to test your production code. This function behaves like runBlocking, but this one skips delays. Because of this you can use delay in your test code and it won’t slow it down. If you check out the documentation of that function, by right-clicking and selecting Go To -> Declaration, you should see more in-depth explanations.

Simply put, the function takes in all the async and launch calls which you do within your test code, that contain delay calls, and advances the time for you, so that you can retrieve the values immediately, instead of waiting for the suspension to end. So if you have an example test, like the snippet in the documentation:

@Test
fun exampleTest() = runTest {
  val deferred = async {
     delay(1_000)
     async {
       delay(1_000)
     }.await()
  }

  deferred.await() // result available immediately
}

You can add any delays between the code, and they should be fast-forwarded. But all of this works with the TestScope, so you have to also learn what that is.

Using Test CoroutineScope and CoroutineContext

To start using runTest, you have to integrate the rest of the test environment for coroutines. Two things will ultimately help you control and affect the coroutines and other suspending functions within your test code. The TestScope and its context. Add the following declarations above your testFetchUserData method:

// 1
  private val testCoroutineDispatcher = StandardTestDispatcher()

// 2
  private val testCoroutineScope =
    TestScope(testCoroutineDispatcher)

These two values will help you dispatch the coroutines in the right contexts and within correct scopes so that you can test them cleanly and correctly. The first is the StandardTestDispatcher, which helps coroutines run immediately, and with the ability to control internal system clocks. There is one caveat here though. async and launch blocks won’t be entered immediately. The exception is if the builders are parameterized with CoroutineStart.UNDISPATCHED. To work around this, you should call yield when inside of the runTest builder.

The second is the TestScope, which exposes all the functions to control the CoroutineDispatcher, and thus changing the execution flow of coroutines. If you pass in the testCoroutineDispatcher to the TestScope constructor, you’ve effectively fully set up the environment you need, to test coroutines.

But you’re still using hardcoded scopes, and contexts, within the MainView, which in turn affects the execution and how well the concurrency is structured. Change MainView.kt to the following:

class MainView(
    private val presenter: MainPresenter,
    private val contextProvider: CoroutineContextProvider,
    private val coroutineScope: CoroutineScope
) {

  var userData: User? = null

  fun fetchUserData() {
    coroutineScope.launch(contextProvider.context()) {
      userData = presenter.getUser("101")
    }
  }

  fun printUserData() {
    println(userData)
  }
}

Instead of hardcoding those two components, you’re now providing them through the constructor, and using the provided values to launch the coroutines. Next, change up the declarations in MainViewTest.kt to the following:

// 1
  private val testCoroutineDispatcher = StandardTestDispatcher()
  // 2
  private val testCoroutineScope = TestScope(testCoroutineDispatcher)
  // 3
  private val testCoroutineContextProvider =
    CoroutineContextProviderImpl(testCoroutineDispatcher)
  // 4
  private val mainPresenter by lazy { MainPresenter() }
  private val mainView by lazy {
    MainView(
      mainPresenter,
      testCoroutineContextProvider,
      testCoroutineScope
    )
  }

Now, you’re using the TestScope with the StandardTestDispatcher, to govern the way MainView is going to start and run coroutines. Finally, you can start using runTest:

@Test
fun testFetchUserData(): Unit = testCoroutineScope.runTest {
  assertNull(mainView.userData)
  mainView.fetchUserData()

  assertEquals("Filip", mainView.userData?.name)
  mainView.printUserData()
}

Build and run the test again. It still fails to compare the values, because the actual value is null again. Why does this happen? Well, the test scope context helps you speed up all the delay calls within nested coroutines, which you start with launch or async. But you’re calling mainPresenter.getUser which doesn’t use those coroutine builders. It’s only marked with suspend so it can use delay internally.

In this case, you need to advance time by yourself, however, this doesn’t mean you can time travel! :]

Advancing Time

When you delay a coroutine, you’re effectively stating how long it will wait until it resumes again. If you want to skip the wait, all you have to do, within a coroutine, is to advance the time by the same amount you’re delaying.

The advancedTimeBy(millis: Long) is a handy extension function on a TestScope which serves that purpose. It advances the internal test clock, so you can skip any amount of delaying you have within your code. To fix the broken test, and fully enable testing of your code, change the test snippet to the following:

@Test
fun testFetchUserData() = testCoroutineScope.runTest {
  assertNull(mainView.userData)
  mainView.fetchUserData()

  // advance the test clock
  advanceTimeBy(1010)

  assertEquals("Filip", mainView.userData?.name)
  mainView.printUserData()
}

By calling advanceTimeBy(1010), you can skip the delay from within MainPresenters getUser code. Make sure to advance the time for a bit more that the actual delay value. This is to make sure that the delayed code gets to resume and finish its execution. You can achieve the same thing by invoking advanceUntilIdle. This runs all of the enqueued tasks until there are no more tasks to run.

Try running the tests now, you should see a positive result! :]

You first check the value to be null, fetching data next, advancing the time so that the value is properly set, finally comparing the value to the expected result, and printing it for the sake of clarity. All in all, a good way to check your code works! You should now be ready to test the rest of your coroutine-related code, in your applications.

Summing it up

Testing coroutines may not be completely straightforward as it is with regular code which uses callbacks, or blocking calls, but there’s a lot of documentation available, and it’s fairly easy to set up. To learn more about the test coroutine helpers and classes, check out the official documentation at the following link: https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test.

This is just a basic setup. In later chapters, you’re going to learn more about testing with coroutines in the context of Android apps.

Key Points

  • Testing code is extremely useful to prove the stability of the software you write.

  • Testing usually involves writing unit and integration tests.

  • Commonly, unit tests validate input and output of functions.

  • Each unit test should cover one unit of code, and be as small as possible.

  • Integration tests, on the other hand, validate interaction between layers and dependencies - the behavior of code.

  • Value-asserting tests are most common and simple to write, as they rely on comparing the result with expected values.

  • A common testing approach is the AAA testing.

  • AAA stands for Arrange, Act and Assert.

  • Arrange sets up all the dependencies and values you need to start testing a unit of code.

  • Act calls the necessary functions to change the data or cause some code behavior, which you will test.

  • Assert compares the behavior or results provided by acting and checks its validity.

  • To test coroutines, you have to set up a coroutine-friendly environment.

  • To set up such an environment, you need to provide a TestScope with StandardTestDispatcher.

  • TestScope takes care of the lifecycle and delays within launch and async blocks, making it easier to execute coroutines.

  • Unit tests should run fast and give results very quickly, without delays.

  • Because of that, you need to force coroutines to be blocking instead of suspending in tests.

  • To force coroutines to be blocking, you can use runTest, on a TestScope.

  • If you have delay calls outside of async or launch blocks, you have to manually advance time.

  • To advance time in a test, call advanceTimeBy(milis: Long), from within a TestScope.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2026 Kodeco Inc.