Skip to content

Latest commit

 

History

History
452 lines (367 loc) · 18 KB

File metadata and controls

452 lines (367 loc) · 18 KB

Module kotlinx-coroutines-test

Test utilities for kotlinx.coroutines.

Overview

This package provides utilities for efficiently testing coroutines.

Name Description
runTest Runs the test code, automatically skipping delays and handling uncaught exceptions.
TestCoroutineScheduler The shared source of virtual time, used for controlling execution order and skipping delays.
TestScope A CoroutineScope that integrates with runTest, providing access to TestCoroutineScheduler.
TestDispatcher A CoroutineDispatcher whose delays are controlled by a TestCoroutineScheduler.
Dispatchers.setMain Mocks the main dispatcher using the provided one. If mocked with a TestDispatcher, its TestCoroutineScheduler is used everywhere by default.

Provided TestDispatcher implementations:

Name Description
StandardTestDispatcher A simple dispatcher with no special behavior other than being linked to a TestCoroutineScheduler.
UnconfinedTestDispatcher A dispatcher that behaves like Dispatchers.Unconfined.

Using in your project

Add kotlinx-coroutines-test to your project test dependencies:

dependencies {
    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3'
}

Do not depend on this project in your main sources, all utilities here are intended and designed to be used only from tests.

Dispatchers.Main Delegation

Dispatchers.setMain will override the Main dispatcher in test scenarios. This is helpful when one wants to execute a test in situations where the platform Main dispatcher is not available, or to replace Dispatchers.Main with a testing dispatcher.

On the JVM, the ServiceLoader mechanism is responsible for overwriting Dispatchers.Main with a testable implementation, which by default will delegate its calls to the real Main dispatcher, if any.

The Main implementation can be overridden using Dispatchers.setMain method with any CoroutineDispatcher implementation, e.g.:

class SomeTest {

    private val mainThreadSurrogate = newSingleThreadContext("UI thread")

    @Before
    fun setUp() {
        Dispatchers.setMain(mainThreadSurrogate)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain() // reset the main dispatcher to the original Main dispatcher
        mainThreadSurrogate.close()
    }

    @Test
    fun testSomeUI() = runBlocking {
        launch(Dispatchers.Main) {  // Will be launched in the mainThreadSurrogate dispatcher
            // ...
        }
    }
}

Calling setMain or resetMain immediately changes the Main dispatcher globally.

If Main is overridden with a TestDispatcher, then its TestCoroutineScheduler is used when new TestDispatcher or TestScope instances are created without TestCoroutineScheduler being passed as an argument.

runTest

runTest is the way to test code that involves coroutines. suspend functions can be called inside it.

IMPORTANT: in order to work with on Kotlin/JS, the result of runTest must be immediately return-ed from each test. The typical invocation of runTest thus looks like this:

@Test
fun testFoo() = runTest {
    // code under test
}

In more advanced scenarios, it's possible instead to use the following form:

@Test
fun testFoo(): TestResult {
    // initialize some test state
    return runTest {
        // code under test
    }
}

runTest is similar to running the code with runBlocking on Kotlin/JVM and Kotlin/Native, or launching a new promise on Kotlin/JS. The main differences are the following:

  • The calls to delay are automatically skipped, preserving the relative execution order of the tasks. This way, it's possible to make tests finish more-or-less immediately.
  • The execution times out after 10 seconds, cancelling the test coroutine to prevent tests from hanging forever and eating up the CI resources.
  • Controlling the virtual time: in case just skipping delays is not sufficient, it's possible to more carefully guide the execution, advancing the virtual time by a duration, draining the queue of the awaiting tasks, or running the tasks scheduled at the present moment.
  • Handling uncaught exceptions spawned in the child coroutines by throwing them at the end of the test.
  • Waiting for asynchronous callbacks. Sometimes, especially when working with third-party code, it's impossible to mock all the dispatchers in use. runTest will handle the situations where some code runs in dispatchers not integrated with the test module.

Timeout

Test automatically time out after 10 seconds. For example, this test will fail with a timeout exception:

@Test
fun testHanging() = runTest {
    CompletableDeferred<Unit>().await() // will hang forever
}

In case the test is expected to take longer than 10 seconds, the timeout can be increased by passing the timeout parameter:

@Test
fun testTakingALongTime() = runTest(timeout = 30.seconds) {
    val result = withContext(Dispatchers.Default) {
        delay(20.seconds) // this delay is not in the test dispatcher and will not be skipped
        3
    }
    assertEquals(3, result)
}

Delay-skipping

To test regular suspend functions, which may have a delay, just run them inside the runTest block.

@Test
fun testFoo() = runTest { // a coroutine with an extra test control
    val actual = foo()
    // ...
}

suspend fun foo() {
    delay(1_000) // when run in `runTest`, will finish immediately instead of delaying
    // ...
}

launch and async

The coroutine dispatcher used for tests is single-threaded, meaning that the child coroutines of the runTest block will run on the thread that started the test, and will never run in parallel.

If several coroutines are waiting to be executed next, the one scheduled after the smallest delay will be chosen. The virtual time will automatically advance to the point of its resumption.

@Test
fun testWithMultipleDelays() = runTest {
    launch {
        delay(1_000)
        println("1. $currentTime") // 1000
        delay(200)
        println("2. $currentTime") // 1200
        delay(2_000)
        println("4. $currentTime") // 3200
    }
    val deferred = async {
        delay(3_000)
        println("3. $currentTime") // 3000
        delay(500)
        println("5. $currentTime") // 3500
    }
    deferred.await()
}

Controlling the virtual time

Inside runTest, the execution is scheduled by TestCoroutineScheduler, which is a virtual time scheduler. The scheduler has several special methods that allow controlling the virtual time:

  • currentTime gets the current virtual time.
  • runCurrent() runs the tasks that are scheduled at this point of virtual time.
  • advanceUntilIdle() runs all enqueued tasks until there are no more.
  • advanceTimeBy(timeDelta) runs the enqueued tasks until the current virtual time advances by timeDelta.
  • timeSource returns a TimeSource that uses the virtual time.
@Test
fun testFoo() = runTest {
    launch {
        val workDuration = testScheduler.timeSource.measureTime {
            println(1)   // executes during runCurrent()
            delay(1_000) // suspends until time is advanced by at least 1_000
            println(2)   // executes during advanceTimeBy(2_000)
            delay(500)   // suspends until the time is advanced by another 500 ms
            println(3)   // also executes during advanceTimeBy(2_000)
            delay(5_000) // will suspend by another 4_500 ms
            println(4)   // executes during advanceUntilIdle()
        }
        assertEquals(6500.milliseconds, workDuration) // the work took 6_500 ms of virtual time
    }
    // the child coroutine has not run yet
    testScheduler.runCurrent()
    // the child coroutine has called println(1), and is suspended on delay(1_000)
    testScheduler.advanceTimeBy(2.seconds) // progress time, this will cause two calls to `delay` to resume
    // the child coroutine has called println(2) and println(3) and suspends for another 4_500 virtual milliseconds
    testScheduler.advanceUntilIdle() // will run the child coroutine to completion
    assertEquals(6500, currentTime) // the child coroutine finished at virtual time of 6_500 milliseconds
}

Using multiple test dispatchers

The virtual time is controlled by an entity called the TestCoroutineScheduler, which behaves as the shared source of virtual time.

Several dispatchers can be created that use the same TestCoroutineScheduler, in which case they will share their knowledge of the virtual time.

To access the scheduler used for this test, use the TestScope.testScheduler property.

@Test
fun testWithMultipleDispatchers() = runTest {
        val scheduler = testScheduler // the scheduler used for this test
        val dispatcher1 = StandardTestDispatcher(scheduler, name = "IO dispatcher")
        val dispatcher2 = StandardTestDispatcher(scheduler, name = "Background dispatcher")
        launch(dispatcher1) {
            delay(1_000)
            println("1. $currentTime") // 1000
            delay(200)
            println("2. $currentTime") // 1200
            delay(2_000)
            println("4. $currentTime") // 3200
        }
        val deferred = async(dispatcher2) {
            delay(3_000)
            println("3. $currentTime") // 3000
            delay(500)
            println("5. $currentTime") // 3500
        }
        deferred.await()
    }

Note: if Dispatchers.Main is replaced by a TestDispatcher, runTest will automatically use its scheduler. This is done so that there is no need to go through the ceremony of passing the correct scheduler to runTest.

Accessing the test coroutine scope

Structured concurrency ties coroutines to scopes in which they are launched. TestScope is a special coroutine scope designed for testing coroutines, and a new one is automatically created for runTest and used as the receiver for the test body.

However, it can be convenient to access a CoroutineScope before the test has started, for example, to perform mocking of some parts of the system in @BeforeTest via dependency injection. In these cases, it is possible to manually create TestScope, the scope for the test coroutines, in advance, before the test begins.

TestScope on its own does not automatically run the code launched in it. In addition, it is stateful in order to keep track of executing coroutines and uncaught exceptions. Therefore, it is important to ensure that TestScope.runTest is called eventually.

val scope = TestScope()

@BeforeTest
fun setUp() {
    Dispatchers.setMain(StandardTestDispatcher(scope.testScheduler))
    TestSubject.setScope(scope)
}

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

@Test
fun testSubject() = scope.runTest {
    // the receiver here is `testScope`
}

Running background work

Sometimes, the fact that runTest waits for all the coroutines to finish is undesired. For example, the system under test may need to receive data from coroutines that always run in the background. Emulating such coroutines by launching them from the test body is not sufficient, because runTest will wait for them to finish, which they never typically do.

For these cases, there is a special coroutine scope: TestScope.backgroundScope. Coroutines launched in it will be cancelled at the end of the test.

@Test
fun testExampleBackgroundJob() = runTest {
  val channel = Channel<Int>()
  backgroundScope.launch {
    var i = 0
    while (true) {
      channel.send(i++)
    }
  }
  repeat(100) {
    assertEquals(it, channel.receive())
  }
}

Eagerly entering launch and async blocks

Some tests only test functionality and don't particularly care about the precise order in which coroutines are dispatched. In these cases, it can be cumbersome to always call runCurrent or yield to observe the effects of the coroutines after they are launched.

If runTest executes with an UnconfinedTestDispatcher, the child coroutines launched at the top level are entered eagerly, that is, they don't go through a dispatch until the first suspension.

@Test
fun testEagerlyEnteringChildCoroutines() = runTest(UnconfinedTestDispatcher()) {
    var entered = false
    val deferred = CompletableDeferred<Unit>()
    var completed = false
    launch {
        entered = true
        deferred.await()
        completed = true
    }
    assertTrue(entered) // `entered = true` already executed.
    assertFalse(completed) // however, the child coroutine then suspended, so it is enqueued.
    deferred.complete(Unit) // resume the coroutine.
    assertTrue(completed) // now the child coroutine is immediately completed.
}

If this behavior is desirable, but some parts of the test still require accurate dispatching, for example, to ensure that the code executes on the correct thread, then simply launch a new coroutine with the StandardTestDispatcher.

@Test
fun testEagerlyEnteringSomeChildCoroutines() = runTest(UnconfinedTestDispatcher()) {
    var entered1 = false
    launch {
        entered1 = true
    }
    assertTrue(entered1) // `entered1 = true` already executed

    var entered2 = false
    launch(StandardTestDispatcher(testScheduler)) {
        // this block and every coroutine launched inside it will explicitly go through the needed dispatches
        entered2 = true
    }
    assertFalse(entered2)
    runCurrent() // need to explicitly run the dispatched continuation
    assertTrue(entered2)
}

Using withTimeout inside runTest

Timeouts are also susceptible to time control, so the code below will immediately finish.

@Test
fun testFooWithTimeout() = runTest {
    assertFailsWith<TimeoutCancellationException> {
        withTimeout(1_000) {
            delay(999)
            delay(2)
            println("this won't be reached")
        }
    }
}

Virtual time support with other dispatchers

Calls to withContext(Dispatchers.IO), withContext(Dispatchers.Default) ,and withContext(Dispatchers.Main) are common in coroutines-based code bases. Unfortunately, just executing code in a test will not lead to these dispatchers using the virtual time source, so delays will not be skipped in them.

suspend fun veryExpensiveFunction() = withContext(Dispatchers.Default) {
    delay(1_000)
    1
}

fun testExpensiveFunction() = runTest {
    val result = veryExpensiveFunction() // will take a whole real-time second to execute
    // the virtual time at this point is still 0
}

Tests should, when possible, replace these dispatchers with a TestDispatcher if the withContext calls delay in the function under test. For example, veryExpensiveFunction above should allow mocking with a TestDispatcher using either dependency injection, a service locator, or a default parameter, if it is to be used with virtual time.

Status of the API

Many parts of the API is experimental, and it is may change before migrating out of experimental (while it is marked as @ExperimentalCoroutinesApi). Changes during experimental may have deprecation applied when possible, but it is not advised to use the API in stable code before it leaves experimental due to possible breaking changes.

If you have any suggestions for improvements to this experimental API please share them on the issue tracker.