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ć

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

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.

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
  }
}
@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()
}
java.lang.AssertionError: 
Expected :Filip
Actual   :null
<Click to see difference>

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.

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.

/**
 * 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
@Test
fun exampleTest() = runTest {
  val deferred = async {
     delay(1_000)
     async {
       delay(1_000)
     }.await()
  }

  deferred.await() // result available immediately
}

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)
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)
  }
}
// 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
    )
  }
@Test
fun testFetchUserData(): Unit = testCoroutineScope.runTest {
  assertNull(mainView.userData)
  mainView.fetchUserData()

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

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.

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

  // advance the test clock
  advanceTimeBy(1010)

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

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.

Key Points

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.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now