Unit testing code that uses coroutines requires some extra attention, as their execution can be asynchronous and happen across multiple threads. This guide covers how suspending functions can be tested, the testing constructs you need to be familiar with, and how to make your code that uses coroutines testable.
The APIs used in this guide are part of the kotlinx.coroutines.test library. Make sure to add the artifact as a test dependency to your project to have access to these APIs.
dependencies {
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
}
Invoking suspending functions in tests
To call suspending functions in tests, you need to be in a coroutine. As JUnit test functions themselves aren't suspending functions, you need to call a coroutine builder inside your tests to start a new coroutine.
runTest
is a coroutine builder designed for testing. Use this to wrap any tests that include coroutines. Note that coroutines can be started not only directly in the test body, but also by the objects being used in the test.
suspend fun fetchData(): String { delay(1000L) return "Hello world" } @Test fun dataShouldBeHelloWorld() = runTest { val data = fetchData() assertEquals("Hello world", data) }
In general, you should have one invocation of runTest
per test, and using an expression body is recommended.
Wrapping your test’s code in runTest
will work for testing basic suspending functions, and it will automatically skip any delays in coroutines, making the test above complete much faster than one second.
However, there are additional considerations to make, depending on what happens in your code under test:
- When your code creates new coroutines other than the top-level test coroutine that
runTest
creates, you’ll need to control how those new coroutines are scheduled by choosing the appropriateTestDispatcher
. - If your code moves the coroutine execution to other dispatchers (for example, by using
withContext
),runTest
will still generally work, but delays will no longer be skipped, and tests will be less predictable as code runs on multiple threads. For these reasons, in tests you should inject test dispatchers to replace real dispatchers.
TestDispatchers
TestDispatchers
are CoroutineDispatcher
implementations for testing purposes. You’ll need to use TestDispatchers
if new coroutines are created during the test to make the execution of the new coroutines predictable.
There are two available implementations of TestDispatcher
: StandardTestDispatcher
and UnconfinedTestDispatcher
, which perform different scheduling of newly-started coroutines. These both use a TestCoroutineScheduler
to control virtual time and manage running coroutines within a test.
There should only be one scheduler instance used in a test, shared between all TestDispatchers
. See Injecting TestDispatchers to learn about sharing schedulers.
To start the top-level test coroutine, runTest
creates a TestScope
, which is an implementation of CoroutineScope
that will always use a TestDispatcher
. If not specified, a TestScope
will create a StandardTestDispatcher
by default, and use that to run the top-level test coroutine.
runTest
keeps track of the coroutines that are queued on the scheduler used by the dispatcher of its TestScope
, and will not return as long as there’s pending work on that scheduler.
StandardTestDispatcher
When you start new coroutines on a StandardTestDispatcher
, they are queued up on the underlying scheduler, to be run whenever the test thread is free to use. To let these new coroutines run, you need to yield the test thread (free it up for other coroutines to use). This queueing behavior gives you precise control over how new coroutines run during the test, and it resembles the scheduling of coroutines in production code.
If the test thread is never yielded during the execution of the top-level test coroutine, any new coroutines will only run after the test coroutine is done (but before runTest
returns):
@Test fun standardTest() = runTest { val userRepo = UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails }
There are several ways to yield the test coroutine to let queued-up coroutines run. All of these calls let other coroutines run on the test thread before returning:
advanceUntilIdle
: Runs all other coroutines on the scheduler until there is nothing left in the queue. This is a good default choice to let all pending coroutines run, and it will work in most test scenarios.advanceTimeBy
: Advances virtual time by the given amount and runs any coroutines scheduled to run before that point in virtual time.runCurrent
: Runs coroutines that are scheduled at the current virtual time.
To fix the previous test, advanceUntilIdle
can be used to let the two pending coroutines perform their work before continuing to the assertion:
@Test fun standardTest() = runTest { val userRepo = UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } advanceUntilIdle() // Yields to perform the registrations assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes }
UnconfinedTestDispatcher
When new coroutines are started on an UnconfinedTestDispatcher
, they are started eagerly on the current thread. This means that they’ll start running immediately, without waiting for their coroutine builder to return. In many cases, this dispatching behavior results in simpler test code, as you don’t need to manually yield the test thread to let new coroutines run.
However, this behavior is different from what you’ll see in production with non-test dispatchers. If your test focuses on concurrency, prefer using StandardTestDispatcher
instead.
To use this dispatcher for the top-level test coroutine in runTest
instead of the default one, create an instance and pass it in as a parameter. This will make new coroutines created within runTest
execute eagerly, as they inherit the dispatcher from the TestScope
.
@Test fun unconfinedTest() = runTest(UnconfinedTestDispatcher()) { val userRepo = UserRepository() launch { userRepo.register("Alice") } launch { userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ✅ Passes }
In this example, the launch calls will start their new coroutines eagerly on the UnconfinedTestDispatcher
, which means that each call to launch will only return after the registration is completed.
Remember that UnconfinedTestDispatcher
starts new coroutines eagerly, but this doesn’t mean that it’ll run them to completion eagerly as well. If the new coroutine suspends, other coroutines will resume executing.
For example, the new coroutine launched within this test will register Alice, but then it suspends when delay
is called. This lets the top-level coroutine proceed with the assertion, and the test fails as Bob is not registered yet:
@Test fun yieldingTest() = runTest(UnconfinedTestDispatcher()) { val userRepo = UserRepository() launch { userRepo.register("Alice") delay(10L) userRepo.register("Bob") } assertEquals(listOf("Alice", "Bob"), userRepo.getAllUsers()) // ❌ Fails }
Injecting test dispatchers
Code under test might use dispatchers to switch threads (using withContext
) or to start new coroutines. When code is executed on multiple threads in parallel, tests can become flaky. It can be difficult to perform assertions at the correct time or to wait for tasks to complete if they’re running on background threads that you have no control over.
In tests, replace these dispatchers with instances of TestDispatchers
. This has several benefits:
- The code will run on the single test thread, making tests more deterministic
- You can control how new coroutines are scheduled and executed
- TestDispatchers use a scheduler for virtual time, which skips delays automatically and lets you advance time manually
Using dependency injection to provide
dispatchers to your classes makes it easy to replace the real dispatchers in
tests. In these examples, we’ll inject a CoroutineDispatcher
, but you can also
inject the broader
CoroutineContext
type, which allows for even more flexibility during tests.
For classes that start coroutines, you can also inject a CoroutineScope
instead of a dispatcher, as detailed in the Injecting a scope
section.
TestDispatchers
will, by default, create a new scheduler when they’re instantiated. Inside runTest
, you can access the testScheduler
property of the TestScope
and pass it in to any newly created TestDispatchers
. This will share their understanding of virtual time, and methods like advanceUntilIdle
will run coroutines on all test dispatchers to completion.
In the following example, you can see a Repository
class that creates a new coroutine using the IO
dispatcher in its initialize
method and switches the caller to the IO
dispatcher in its fetchData
method:
// Example class demonstrating dispatcher use cases class Repository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { private val scope = CoroutineScope(ioDispatcher) val initialized = AtomicBoolean(false) // A function that starts a new coroutine on the IO dispatcher fun initialize() { scope.launch { initialized.set(true) } } // A suspending function that switches to the IO dispatcher suspend fun fetchData(): String = withContext(ioDispatcher) { require(initialized.get()) { "Repository should be initialized first" } delay(500L) "Hello world" } }
In tests, you can inject a TestDispatcher
implementation to replace the IO
dispatcher.
In the example below, we inject a StandardTestDispatcher
into the repository, and use advanceUntilIdle
to make sure that the new coroutine started in initialize
completes before proceeding.
fetchData
will also benefit from running on a TestDispatcher
, as it will run on the test thread and skip the delay it contains during the test.
class RepositoryTest { @Test fun repoInitWorksAndDataIsHelloWorld() = runTest { val dispatcher = StandardTestDispatcher(testScheduler) val repository = Repository(dispatcher) repository.initialize() advanceUntilIdle() // Runs the new coroutine assertEquals(true, repository.initialized.get()) val data = repository.fetchData() // No thread switch, delay is skipped assertEquals("Hello world", data) } }
New coroutines started on a TestDispatcher
can be advanced manually as shown above with initialize
. Note, however, that this would not be possible or desirable in production code. Instead, this method should be redesigned to be either suspending (for sequential execution), or to return a Deferred
value (for concurrent execution).
For example, you can use async
to start a new coroutine and create a Deferred
:
class BetterRepository(private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO) { private val scope = CoroutineScope(ioDispatcher) fun initialize() = scope.async { // ... } }
This lets you safely await
the completion of this code in both tests and production code:
@Test fun repoInitWorks() = runTest { val dispatcher = StandardTestDispatcher(testScheduler) val repository = BetterRepository(dispatcher) repository.initialize().await() // Suspends until the new coroutine is done assertEquals(true, repository.initialized.get()) // ... }
runTest
will wait for pending coroutines to complete before returning if the coroutines are on a TestDispatcher
that it shares a scheduler with. It will also wait for coroutines that are children of the top-level test coroutine, even if they’re on other dispatchers (up to a timeout specified by the dispatchTimeoutMs
parameter, which is 60 seconds by default).
Setting the Main dispatcher
In local unit tests, the Main
dispatcher that wraps the Android UI thread will be unavailable, as these tests are executed on a local JVM and not an Android device. If your code under test references the main thread, it’ll throw an exception during unit tests.
In some cases, you can inject the Main
dispatcher the same way as other dispatchers, as described in the previous section, allowing you to replace it with a TestDispatcher
in tests. However, some APIs such as viewModelScope
use a hardcoded Main
dispatcher under the hood.
Here’s an example of a ViewModel
implementation that uses viewModelScope
to launch a coroutine that loads data:
class HomeViewModel : ViewModel() { private val _message = MutableStateFlow("") val message: StateFlow<String> get() = _message fun loadMessage() { viewModelScope.launch { _message.value = "Greetings!" } } }
To replace the Main
dispatcher with a TestDispatcher
in all cases, use the Dispatchers.setMain
and Dispatchers.resetMain
functions.
class HomeViewModelTest { @Test fun settingMainDispatcher() = runTest { val testDispatcher = UnconfinedTestDispatcher(testScheduler) Dispatchers.setMain(testDispatcher) try { val viewModel = HomeViewModel() viewModel.loadMessage() // Uses testDispatcher, runs its coroutine eagerly assertEquals("Greetings!", viewModel.message.value) } finally { Dispatchers.resetMain() } } }
If the Main
dispatcher has been replaced with a TestDispatcher
, any newly-created TestDispatchers
will automatically use the scheduler from the Main
dispatcher, including the StandardTestDispatcher
created by runTest
if no other dispatcher is passed to it.
This makes it easier to ensure that there is only a single scheduler in use during the test. For this to work, make sure to create all other TestDispatcher
instances after calling Dispatchers.setMain
.
A common pattern to avoid duplicating the code that replaces the Main
dispatcher in each test is to extract it into a JUnit test rule:
// Reusable JUnit4 TestRule to override the Main dispatcher class MainDispatcherRule( val testDispatcher: TestDispatcher = UnconfinedTestDispatcher(), ) : TestWatcher() { override fun starting(description: Description) { Dispatchers.setMain(testDispatcher) } override fun finished(description: Description) { Dispatchers.resetMain() } } class HomeViewModelTestUsingRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun settingMainDispatcher() = runTest { // Uses Main’s scheduler val viewModel = HomeViewModel() viewModel.loadMessage() assertEquals("Greetings!", viewModel.message.value) } }
This rule implementation uses an UnconfinedTestDispatcher
by default, but a StandardTestDispatcher
can be passed in as a parameter if the Main
dispatcher shouldn’t execute eagerly in a given test class.
When you need a TestDispatcher
instance in the test body, you can reuse the testDispatcher
from the rule, as long as it’s the desired type. If you want to be explicit about the type of TestDispatcher
used in the test, or if you need a TestDispatcher
that’s a different type than the one used for Main
, you can create a new TestDispatcher
within runTest
. As the Main
dispatcher is set to a TestDispatcher
, any newly created TestDispatchers
will share its scheduler automatically.
class DispatcherTypesTest { @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun injectingTestDispatchers() = runTest { // Uses Main’s scheduler // Use the UnconfinedTestDispatcher from the Main dispatcher val unconfinedRepo = Repository(mainDispatcherRule.testDispatcher) // Create a new StandardTestDispatcher (uses Main’s scheduler) val standardRepo = Repository(StandardTestDispatcher()) } }
Creating dispatchers outside a test
In some cases, you might need a TestDispatcher
to be available outside the test method. For example, during the initialization of a property in the test class:
class ExampleRepository(private val ioDispatcher: CoroutineDispatcher) { /* ... */ } class RepositoryTestWithRule { private val repository = ExampleRepository(/* What TestDispatcher? */) @get:Rule val mainDispatcherRule = MainDispatcherRule() @Test fun someRepositoryTest() = runTest { // Test the repository... // ... } }
If you’re replacing the Main
dispatcher as shown in the previous section, TestDispatchers
created after the Main
dispatcher has been replaced will automatically share its scheduler.
This isn’t the case, however, for TestDispatchers
created as properties of the test class or TestDispatchers
created during the initialization of properties in the test class. These are initialized before the Main
dispatcher is replaced. Therefore, they would create new schedulers.
To make sure that there’s only one scheduler in your test, create the MainDispatcherRule
property first. Then reuse its dispatcher (or its scheduler, if you need a TestDispatcher
of a different type) in the initializers of other class-level properties as needed.
class RepositoryTestWithRule { @get:Rule val mainDispatcherRule = MainDispatcherRule() private val repository = ExampleRepository(mainDispatcherRule.testDispatcher) @Test fun someRepositoryTest() = runTest { // Takes scheduler from Main // Any TestDispatcher created here also takes the scheduler from Main val newTestDispatcher = StandardTestDispatcher() // Test the repository... } }
Note that both runTest
and TestDispatchers
created within the test will still automatically share the scheduler of the Main
dispatcher.
If you’re not replacing the Main
dispatcher, create your first TestDispatcher
(which creates a new scheduler) as a property of the class. Then, manually pass that scheduler to each runTest
invocation and each new TestDispatcher
created, both as properties and within the test:
class RepositoryTest { // Creates the single test scheduler private val testDispatcher = UnconfinedTestDispatcher() private val repository = ExampleRepository(testDispatcher) @Test fun someRepositoryTest() = runTest(testDispatcher.scheduler) { // Take the scheduler from the TestScope val newTestDispatcher = UnconfinedTestDispatcher(this.testScheduler) // Or take the scheduler from the first dispatcher, they’re the same val anotherTestDispatcher = UnconfinedTestDispatcher(testDispatcher.scheduler) // Test the repository... } }
In this sample, the scheduler from the first dispatcher is passed to runTest
. This will create a new StandardTestDispatcher
for the TestScope
using that scheduler. You could also pass in the dispatcher to runTest
directly to run the test coroutine on that dispatcher.
Creating your own TestScope
Like with TestDispatchers
, you might need to access a TestScope
outside the test body. While runTest
creates a TestScope
under the hood automatically, you can also create your own TestScope
to use with runTest
.
When doing this, make sure to call runTest
on the TestScope
you’ve created:
class SimpleExampleTest { val testScope = TestScope() // Creates a StandardTestDispatcher @Test fun someTest() = testScope.runTest { // ... } }
The code above creates a StandardTestDispatcher
for the TestScope
implicitly, as well as a new scheduler. These objects can all also be created explicitly. This can be useful if you need to integrate it with dependency injection setups.
class ExampleTest { val testScheduler = TestCoroutineScheduler() val testDispatcher = StandardTestDispatcher(testScheduler) val testScope = TestScope(testDispatcher) @Test fun someTest() = testScope.runTest { // ... } }
Injecting a scope
If you have a class that creates coroutines that you need to control during
tests, you can inject a coroutine scope into that class, replacing it with a
TestScope
in tests.
In the following example, the UserState
class depends on a UserRepository
to register new users and to fetch the list of registered users. As these calls
to UserRepository
are suspending function calls, UserState
uses the injected
CoroutineScope
to start a new coroutine inside its registerUser
function.
class UserState( private val userRepository: UserRepository, private val scope: CoroutineScope, ) { private val _users = MutableStateFlow(emptyList<String>()) val users: StateFlow<List<String>> = _users.asStateFlow() fun registerUser(name: String) { scope.launch { userRepository.register(name) _users.update { userRepository.getAllUsers() } } } }
To test this class, you can pass in the TestScope
from runTest
when creating
the UserState
object:
class UserStateTest { @Test fun addUserTest() = runTest { // this: TestScope val repository = FakeUserRepository() val userState = UserState(repository, scope = this) userState.registerUser("Mona") advanceUntilIdle() // Let the coroutine complete and changes propagate assertEquals(listOf("Mona"), userState.users.value) } }
To inject a scope outside of the test function, for example into an object under test that's created as a property in the test class, see Creating your own TestScope.