free geoip
41

Fixing Kotlin Coroutine Test TimeoutException

When working with Kotlin coroutines in unit tests, developers often encounter the dreaded TimeoutCancellationException or TestCoroutineDispatcher issues. These problems usually…

When working with Kotlin coroutines in unit tests, developers often encounter the dreaded TimeoutCancellationException or TestCoroutineDispatcher issues. These problems usually occur when a coroutine never finishes within the specified timeout, causing your tests to fail unexpectedly. In this article, we will break down why Kotlin Coroutine Test TimeoutException happens, explore common causes, and walk through practical solutions with complete code examples.

Fixing Kotlin Coroutine Test TimeoutException

1. Understanding Kotlin Coroutine Timeout in Tests

The TimeoutCancellationException happens when the coroutine under test takes longer than expected or gets stuck waiting for a job that never completes. In coroutine testing, time is controlled artificially, so improper usage can cause tests to hang indefinitely.

For example:

import kotlinx.coroutines.*
import kotlinx.coroutines.test.runTest
import kotlin.test.Test

class TimeoutExampleTest {

    @Test
    fun sampleTestWithTimeout() = runTest {
        withTimeout(1000) { // Timeout in milliseconds
            delay(2000) // This will trigger TimeoutCancellationException
            println("This line will not be reached")
        }
    }
}

In this code, delay(2000) exceeds the 1-second timeout, resulting in an exception. While this is expected, similar errors can occur unintentionally if your coroutine logic is flawed.

2. Common Causes of TimeoutException in Coroutine Tests

  • Using delay() without advancing virtual time in tests
  • Infinite loops or uncompleted jobs
  • Network calls without proper mocking
  • Missing advanceUntilIdle() or advanceTimeBy() in unit tests
  • Improper usage of Dispatchers.Main in tests without setting a test dispatcher

3. Setting Up Coroutine Test Environment Properly

Kotlin’s kotlinx.coroutines.test library provides tools to control coroutine execution during tests. This helps you run coroutines deterministically without unexpected delays.

Install the dependency in your build.gradle:

testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.8.0"

3.1 Example of Correct Coroutine Test Setup

import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.delay
import kotlinx.coroutines.test.*
import kotlin.test.AfterTest
import kotlin.test.BeforeTest
import kotlin.test.Test

@OptIn(ExperimentalCoroutinesApi::class)
class CoroutineTestExample {

    private val testDispatcher = StandardTestDispatcher()

    @BeforeTest
    fun setup() {
        Dispatchers.setMain(testDispatcher)
    }

    @AfterTest
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun testCoroutineWithoutTimeoutFailure() = runTest {
        launch {
            delay(1000) // Simulated async work
            println("Finished work")
        }
        advanceUntilIdle() // Runs all queued coroutines until completion
    }
}

In the example above:

  • We replace Dispatchers.Main with a StandardTestDispatcher
  • We use advanceUntilIdle() to avoid TimeoutException
  • No real delays happen, since the test scheduler advances virtual time instantly

4. Fixing an Actual TimeoutException in a Real Case

Imagine we are testing a repository function that fetches user data from a network API:

class UserRepository(private val api: UserApi) {
    suspend fun getUserData(): String {
        delay(2000) // Simulate slow API call
        return api.fetchUser()
    }
}

Test code without proper handling might fail:

@Test
fun testGetUserDataFails() = runTest {
    val api = mockk<UserApi> {
        coEvery { fetchUser() } returns "John Doe"
    }
    val repo = UserRepository(api)

    val result = repo.getUserData() // Might cause TimeoutException
    assertEquals("John Doe", result)
}

Why does it fail? The delay(2000) is real, not virtual. The solution is to advance virtual time:

@Test
fun testGetUserDataPasses() = runTest {
    val api = mockk<UserApi> {
        coEvery { fetchUser() } returns "John Doe"
    }
    val repo = UserRepository(api)

    val job = launch { 
        val result = repo.getUserData()
        assertEquals("John Doe", result)
    }

    advanceTimeBy(2000) // Skip virtual delay
    job.join()
}

5. Best Practices to Avoid TimeoutException

  • Always use runTest from kotlinx.coroutines.test
  • Replace Dispatchers.Main with a test dispatcher
  • Use advanceUntilIdle() or advanceTimeBy() instead of real delays
  • Mock network and database calls to avoid long waits
  • Keep coroutine jobs cancelable to prevent infinite waits

6. Conclusion

Kotlin Coroutine Test TimeoutException is a common pitfall for developers writing coroutine-based tests. By understanding virtual time, properly setting up test dispatchers, and advancing time manually, you can write deterministic, fast, and reliable coroutine tests.

For more details on coroutine testing, check the official documentation: Kotlin Coroutines Test Guide.

rysasahrial

Leave a Reply

Your email address will not be published. Required fields are marked *