Test utilities for kotlinx.coroutines
.
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. |
Add kotlinx-coroutines-test
to your project test dependencies:
dependencies {
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0-RC.2'
}
Do not depend on this project in your main sources, all utilities here are intended and designed to be used only from tests.
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 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 60 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.
Test automatically time out after 60 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 60 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)
}
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
// ...
}
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()
}
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 bytimeDelta
.timeSource
returns aTimeSource
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
}
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.
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`
}
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())
}
}
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)
}
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")
}
}
}
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.
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.