DEV Community

Cover image for Unit Testing with Coroutines in Kotlin: Tools and Best Practices
Alan Gomes for Comunidade Dev Space

Posted on

Unit Testing with Coroutines in Kotlin: Tools and Best Practices

1 – Introduction

Testing asynchronous code can be challenging. Functions that use coroutines can have unpredictable behavior due to delays, concurrent execution, and context switches. Fortunately, Kotlin provides built-in tools and helper libraries to simplify unit testing with coroutines .

In this article, we will explore how to use tools like UnconfinedTestDispatcher, TestCoroutineScheduler, and the Turbine library to efficiently test coroutines and flows.


2 – The Problem of Testing Asynchronous Code

Testing asynchronous code is tricky because:

  1. Coroutines can run in different threads or contexts.
  2. Methods like delay or withTimeout introduce real waits that can slow down tests.
  3. Flows depend on asynchronous events that need to be controlled.

Without the right tools, tests can become flaky or take longer than necessary.


3 - Kotlin Native Tools

3.1 – UnconfinedTestDispatcher

  • What is it? A dispatcher designed for testing that is not tied to specific threads.
  • Why use it? Allows you to run coroutines without context restrictions, making tests predictable.
  • Example:
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*

fun main() = runTest {
    // The testScheduler is a TestCoroutineScheduler automatically provided by runTest .
// It manages virtual time for all coroutines in this test.
    val testDispatcher = UnconfinedTestDispatcher(testScheduler)

// We use testDispatcher to ensure that all operations share not only the same scheduler , but also the same dispatcher .
    withContext(testDispatcher) {
        println(" Running in UnconfinedTestDispatcher : ${Thread.currentThread().name}")
        delay(1000) // Simulates a virtual 1 second delay
        println("Finalizing in UnconfinedTestDispatcher .")
    }
}
Enter fullscreen mode Exit fullscreen mode

3.2 – TestCoroutineScheduler

  • What is it? A scheduler that allows you to manually advance time in tests.
  • Why use it? To control time-based tasks (delay, withTimeout) without waiting for real time to pass.
  • Example:
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*
import org.junit.Test

class CoroutinesTests {
    // Create a TestCoroutineScheduler manually
    private val scheduler = TestCoroutineScheduler()

    // Create a dispatcher based on the manual scheduler
    private var testDispatcher: TestDispatcher = UnconfinedTestDispatcher(scheduler)

    @Test
    fun testCoroutines() {
// Use runTest with the dispatcher configured
        runTest(testDispatcher) {
            println("Task started.")

// Launch a coroutine in the same dispatcher
            delay(1000) // Simulates a virtual 1 second delay
            println("Task completed.")

// Advance time virtually by 1 second
            scheduler.advanceTimeBy(1000)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Why do we need @Test?

  • The @Test annotation is used to mark a method as a test case, allowing it to be executed by JUnit.
  • Without @Test, the testCoroutines method would be treated as a normal method and would not appear as executable in the IDE.
  • When JUnit detects @Test, it:
    1. Automatically instantiate the test class.
    2. Execute the marked method, with all dependencies configured (such as scheduler and testDispatcher).

3.3 – runTest

  • What is it? A function that provides a controlled environment for running coroutines in tests.
  • Why use it? Simplifies the setup of asynchronous tests by replacing runBlocking in tests.
  • Example:
import kotlinx.coroutines.*
import kotlinx.coroutines.test.*

fun main() = runTest {
    println("Starting test.")
    delay(1000) // This does not delay the actual test
    println("Test completed.")
}
Enter fullscreen mode Exit fullscreen mode

4 - Testing flows with the turbine library

For Flows, the Turbine library simplifies the collection and validation of emitted values.

4.1 – What is Turbine?

Turbine is a library designed specifically for testing flows in Kotlin , allowing you to validate emitted items and events like cancellation or completion.

  • Example with turbine:
import app.cash.turbine.test
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.test.runTest

fun main() = runTest {
    val flow = flow {
        emit(1)
        delay(1000)
        emit(2)
    }

    flow.test {
        val item1 = awaitItem()
        println("Received: $item1") // Display the first value on the terminal
        assert(item1 == 1) // Validate the first item

        val item2 = awaitItem()
        println("Received: $item2") // Display the second value on the terminal
        assert(item2 == 2) // Validate the second item

        awaitComplete() // Confirms that the flow has completed
        println("Flow completed !") // Indicates that the flow has finished
    }
}
Enter fullscreen mode Exit fullscreen mode

5 – Tool Comparison

Tool Function When to use
UnconfinedTestDispatcher Runs coroutines without context restrictions. Simple tests that do not rely on real-time.
TestCoroutineScheduler Manually controls timing in tests. Tests with delay, timeout, or time events.
Turbine Tests values emitted by Flow. Flow-specific tests.

6 – Conclusion

Testing coroutines and flows in Kotlin can be challenging, but with the right tools, you can create fast, efficient, and reliable tests. Using UnconfinedTestDispatcher , TestCoroutineScheduler , and libraries like Turbine makes the process much simpler.

Summary:

  1. Time tracking: Use TestCoroutineScheduler to manage delays and scheduling.
  2. Test environments: runTest is the basis for asynchronous testing.
  3. Flow Testing: Turbine is the ideal solution for validating flows.

Now that you know the best practices for testing coroutines in Kotlin , try applying them to your projects!

In the next article, we will discuss a little more about examples of asynchrony, just to explore the subject a little more and fix the content better.

References:
Official Kotlin documentation on coroutines
Turbine Documentation

Top comments (0)