article banner

Testing Kotlin Coroutines

This is a chapter from the book Kotlin Coroutines. You can find it on LeanPub or Amazon.

Testing suspending functions in most cases is not different from testing normal functions. Take a look at the fetchUserData below from FetchUserUseCase. Checking whether it shows data as expected can be easily achieved thanks to a few fakes1 (or mocks2) and simple assertions.

import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.runBlocking import org.junit.Test import kotlin.test.assertEquals //sampleStart class FetchUserUseCase( private val repo: UserDataRepository, ) { suspend fun fetchUserData(): User = coroutineScope { val name = async { repo.getName() } val friends = async { repo.getFriends() } val profile = async { repo.getProfile() } User( name = name.await(), friends = friends.await(), profile = profile.await() ) } } class FetchUserDataTest { @Test fun `should construct user`() = runBlocking { // given val repo = FakeUserDataRepository() val useCase = FetchUserUseCase(repo) // when val result = useCase.fetchUserData() // then val expectedUser = User( name = "Ben", friends = listOf(Friend("some-friend-id-1")), profile = Profile("Example description") ) assertEquals(expectedUser, result) } class FakeUserDataRepository : UserDataRepository { override suspend fun getName(): String = "Ben" override suspend fun getFriends(): List<Friend> = listOf(Friend("some-friend-id-1")) override suspend fun getProfile(): Profile = Profile("Example description") } } //sampleEnd interface UserDataRepository { suspend fun getName(): String suspend fun getFriends(): List<Friend> suspend fun getProfile(): Profile } data class User( val name: String, val friends: List<Friend>, val profile: Profile ) data class Friend(val id: String) data class Profile(val description: String)

My method for testing logic should not be used as a reference. There are many conflicting ideas for how tests should look. I’ve used fakes here instead of mocks so as not to introduce any external library (I also personally prefer them). I’ve also tried to keep all tests minimalistic to make them easier to read.

Similarly, in many other cases, if we are interested in what the suspending function does, we practically need nothing else but runBlocking and classic tools for asserting. This is what unit tests look like in many projects. Here are a few unit tests from Kt. Academy backend:

class UserTests : KtAcademyFacadeTest() { @Test fun `should modify user details`() = runBlocking { // given thereIsUser(aUserToken, aUserId) // when facade.updateUserSelf( aUserToken, PatchUserSelfRequest( bio = aUserBio, bioPl = aUserBioPl, publicKey = aUserPublicKey, customImageUrl = aCustomImageUrl ) ) // then with(findUser(aUserId)) { assertEquals(aUserBio, bio) assertEquals(aUserBioPl, bioPl) assertEquals(aUserPublicKey, publicKey) assertEquals(aCustomImageUrl, customImageUrl) } } //... }

Integration tests can be implemented in the same way. We just use runBlocking, and there is nearly no difference between testing how suspending and blocking functions behave.

Testing time dependencies

The difference arises when we want to start testing time dependencies. For example, think of the following functions:

suspend fun produceCurrentUserSeq(): User { val profile = repo.getProfile() val friends = repo.getFriends() return User(profile, friends) } suspend fun produceCurrentUserSym(): User = coroutineScope { val profile = async { repo.getProfile() } val friends = async { repo.getFriends() } User(profile.await(), friends.await()) }

Both functions produce the same result; the difference is that the first one does it sequentially, while the second one does it simultaneously. The difference is that if fetching the profile and the friends takes 1 second each, then the first function would require around 2 seconds, whereas the first would require only 1. How would you test this?

Notice that the difference arises only when execution of getProfile and getFriends truly takes some time. If they are immediate, both ways of producing the user are indistinguishable. So, we might help ourselves by delaying fake functions using delay to simulate a delayed data loading scenario:

class FakeDelayedUserDataRepository : UserDataRepository { override suspend fun getProfile(): Profile { delay(1000) return Profile("Example description") } override suspend fun getFriends(): List<Friend> { delay(1000) return listOf(Friend("some-friend-id-1")) } }

Now, the difference will be visible in unit tests: the produceCurrentUserSeq call will take around 1 second, and the produceCurrentUserSym call will take around 2 seconds. The problem is that we do not want a single unit test to take so much time. We typically have thousands of unit tests in our projects, and we want all of them to execute as quickly as possible. How to have your cake and eat it too? For that, we need to operate in simulated time. Here comes the kotlinx-coroutines-test library to the rescue with its StandardTestDispatcher.

This chapter presents the kotlinx-coroutines-test functions and classes introduced in version 1.6. If you use an older version of this library, in most cases it should be enough to use runBlockingTest instead of runTest, TestCoroutineDispatcher instead of StandardTestDispatcher, and TestCoroutineScope instead of TestScope. Also, advanceTimeBy in older versions is like advanceTimeBy and runCurrent in versions newer than 1.6. The detailed differences are described in the migration guide at https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/MIGRATION.md.

TestCoroutineScheduler and StandardTestDispatcher

When we call delay, our coroutine is suspended and resumed after a set time. This behavior can be altered thanks to TestCoroutineScheduler from kotlinx-coroutines-test, which makes delay operate in virtual time, which is fully simulated and does not depend on real time.

fun main() { val scheduler = TestCoroutineScheduler() println(scheduler.currentTime) // 0 scheduler.advanceTimeBy(1_000) println(scheduler.currentTime) // 1000 scheduler.advanceTimeBy(1_000) println(scheduler.currentTime) // 2000 }

TestCoroutineScheduler as well as StandardTestDispatcher, TestScope and runTest are still experimental.

To use TestCoroutineScheduler on coroutines, we should use a dispatcher that supports it. The standard option is StandardTestDispatcher. Unlike most dispatchers, it is not used just to decide on which thread a coroutine should run. Coroutines started with such a dispatcher will not run until we advance virtual time. The most typical way to do this is by using advanceUntilIdle, which advances virtual time and invokes all the operations that would be called during that time if this were real time.

fun main() { val scheduler = TestCoroutineScheduler() val testDispatcher = StandardTestDispatcher(scheduler) CoroutineScope(testDispatcher).launch { println("Some work 1") delay(1000) println("Some work 2") delay(1000) println("Coroutine done") } println("[${scheduler.currentTime}] Before") scheduler.advanceUntilIdle() println("[${scheduler.currentTime}] After") } // [0] Before // Some work 1 // Some work 2 // Coroutine done // [2000] After

StandardTestDispatcher creates TestCoroutineScheduler by default, so we do not need to do so explicitly. We can access it with the scheduler property.

fun main() { val dispatcher = StandardTestDispatcher() CoroutineScope(dispatcher).launch { println("Some work 1") delay(1000) println("Some work 2") delay(1000) println("Coroutine done") } println("[${dispatcher.scheduler.currentTime}] Before") dispatcher.scheduler.advanceUntilIdle() println("[${dispatcher.scheduler.currentTime}] After") } // [0] Before // Some work 1 // Some work 2 // Coroutine done // [2000] After

It is important to notice that StandardTestDispatcher does not advance time by itself. We need to do this, otherwise our coroutine will never be resumed.

fun main() { val testDispatcher = StandardTestDispatcher() runBlocking(testDispatcher) { delay(1) println("Coroutine done") } } // (code runs forever)

Another way to push time is using advanceTimeBy with a concrete number of milliseconds. This function advances time and executes all operations that happened in the meantime. This means that if we push by 2 milliseconds, everything that was delayed by less than that time will be resumed. To resume operations scheduled exactly at the second millisecond, we need to additionally invoke the runCurrent function.

fun main() { val testDispatcher = StandardTestDispatcher() CoroutineScope(testDispatcher).launch { delay(1) println("Done1") } CoroutineScope(testDispatcher).launch { delay(2) println("Done2") } testDispatcher.scheduler.advanceTimeBy(2) // Done testDispatcher.scheduler.runCurrent() // Done2 }

Here is a bigger example of using advanceTimeBy together with runCurrent.

fun main() { val testDispatcher = StandardTestDispatcher() CoroutineScope(testDispatcher).launch { delay(2) print("Done") } CoroutineScope(testDispatcher).launch { delay(4) print("Done2") } CoroutineScope(testDispatcher).launch { delay(6) print("Done3") } for (i in 1..5) { print(".") testDispatcher.scheduler.advanceTimeBy(1) testDispatcher.scheduler.runCurrent() } } // ..Done..Done2.

How does it work under the hood? When delay is called, it checks if the dispatcher (class with ContinuationInterceptor key) implements the Delay interface (StandardTestDispatcher does). For such dispatchers, it calls their scheduleResumeAfterDelay function instead of the one from the DefaultDelay, which waits in real time.

To see that virtual time is truly independent of real time, see the example below. Adding Thread.sleep will not influence the coroutine with StandardTestDispatcher. Note also that the call to advanceUntilIdle takes only a few milliseconds, so it does not wait for any real time. It immediately pushes the virtual time and executes coroutine operations.

fun main() { val dispatcher = StandardTestDispatcher() CoroutineScope(dispatcher).launch { delay(1000) println("Coroutine done") } Thread.sleep(Random.nextLong(2000)) // Does not matter // how much time we wait here, it will not influence // the result val time = measureTimeMillis { println("[${dispatcher.scheduler.currentTime}] Before") dispatcher.scheduler.advanceUntilIdle() println("[${dispatcher.scheduler.currentTime}] After") } println("Took $time ms") } // [0] Before // Coroutine done // [1000] After // Took 15 ms (or other small number)

In the previous examples, we were using StandardTestDispatcher and wrapping it with a scope. Instead, we could use TestScope, which does the same (and it collects all exceptions with CoroutineExceptionHandler). The trick is that on this scope we can also use functions like advanceUntilIdle, advanceTimeBy, or the currentTime property , all of which are delegated to the scheduler used by this scope. This is very convenient.

fun main() { val scope = TestScope() scope.launch { delay(1000) println("First done") delay(1000) println("Coroutine done") } println("[${scope.currentTime}] Before") // [0] Before scope.advanceTimeBy(1000) scope.runCurrent() // First done println("[${scope.currentTime}] Middle") // [1000] Middle scope.advanceUntilIdle() // Coroutine done println("[${scope.currentTime}] After") // [2000] After }

We will later see that StandardTestDispatcher is often used directly on Android to test ViewModels, Presenters, Fragments, etc. We could also use it to test the produceCurrentUserSeq and produceCurrentUserSym functions by starting them in a coroutine, advancing time until idle, and checking how much simulated time they took. However, this would be quite complicated; instead, we should use runTest, which is designed for such purposes.

runTest

runTest is the most commonly used function from kotlinx-coroutines-test. It starts a coroutine with TestScope and immediately advances it until idle. Within its coroutine, the scope is of type TestScope, so we can check currentTime at any point. Therefore, we can check how time flows in our coroutines, while our tests take milliseconds.

class TestTest { @Test fun test1() = runTest { assertEquals(0, currentTime) delay(1000) assertEquals(1000, currentTime) } @Test fun test2() = runTest { assertEquals(0, currentTime) coroutineScope { launch { delay(1000) } launch { delay(1500) } launch { delay(2000) } } assertEquals(2000, currentTime) } }

Let's get back to our functions, where we loaded user data sequentially and simultaneously. With runTest, testing them is easy. Assuming that our fake repository needs 1 second for each function call, sequential processing should take 2 seconds, and simultaneous processing should take only 1. Thanks to the fact we are using virtual time, our tests are immediate, and the values of currentTime are precise.

@Test fun `Should produce user sequentially`() = runTest { // given val userDataRepository = FakeDelayedUserDataRepository() val useCase = ProduceUserUseCase(userDataRepository) // when useCase.produceCurrentUserSeq() // then assertEquals(2000, currentTime) } @Test fun `Should produce user simultaneously`() = runTest { // given val userDataRepository = FakeDelayedUserDataRepository() val useCase = ProduceUserUseCase(userDataRepository) // when useCase.produceCurrentUserSym() // then assertEquals(1000, currentTime) }

Since it is an important use case, let's see a full example of testing a sequential function with all required classes and interfaces:

class FetchUserUseCase( private val repo: UserDataRepository, ) { suspend fun fetchUserData(): User = coroutineScope { val name = async { repo.getName() } val friends = async { repo.getFriends() } val profile = async { repo.getProfile() } User( name = name.await(), friends = friends.await(), profile = profile.await() ) } } class FetchUserDataTest { @Test fun `should load data concurrently`() = runTest { // given val userRepo = FakeUserDataRepository() val useCase = FetchUserUseCase(userRepo) // when useCase.fetchUserData() // then assertEquals(1000, currentTime) } @Test fun `should construct user`() = runTest { // given val userRepo = FakeUserDataRepository() val useCase = FetchUserUseCase(userRepo) // when val result = useCase.fetchUserData() // then val expectedUser = User( name = "Ben", friends = listOf(Friend("some-friend-id-1")), profile = Profile("Example description") ) assertEquals(expectedUser, result) } class FakeUserDataRepository : UserDataRepository { override suspend fun getName(): String { delay(1000) return "Ben" } override suspend fun getFriends(): List<Friend> { delay(1000) return listOf(Friend("some-friend-id-1")) } override suspend fun getProfile(): Profile { delay(1000) return Profile("Example description") } } } interface UserDataRepository { suspend fun getName(): String suspend fun getFriends(): List<Friend> suspend fun getProfile(): Profile } data class User( val name: String, val friends: List<Friend>, val profile: Profile ) data class Friend(val id: String) data class Profile(val description: String)

runTest includes TestScope, that includes StandardTestDispatcher, that includes TestCoroutineScheduler.

Background scope

The runTest function creates a scope; like all such functions, it awaits the completion of its children. This means that if you start a process that never ends, your test will never stop.

@Test fun `should increment counter`() = runTest { var i = 0 launch { while (true) { delay(1000) i++ } } delay(1001) assertEquals(1, i) delay(1000) assertEquals(2, i) // Test would pass if we added // coroutineContext.job.cancelChildren() }

For such situations, runTest offers backgroundScope. This is a scope that also operates on virtual time, but runTest will not await its completion. This is why the test below passes without any problems. We use backgroundScope to start all the processes we don't want our test to wait for.

@Test fun `should increment counter`() = runTest { var i = 0 backgroundScope.launch { while (true) { delay(1000) i++ } } delay(1001) assertEquals(1, i) delay(1000) assertEquals(2, i) }

Testing cancellation and context passing

If you want to test whether a certain function respects structured concurrency, the easiest way is to capture the context from a suspending function, then check if it contains the expected value or if its job has the appropriate state. As an example, consider the mapAsync function, which I explain in more detail in the Recipes chapter.

suspend fun <T, R> Iterable<T>.mapAsync( transformation: suspend (T) -> R ): List<R> = coroutineScope { this@mapAsync.map { async { transformation(it) } } .awaitAll() }

This function should map elements asynchronously while preserving their order. This behavior can be verified by the following test:

@Test fun `should map async and keep elements order`() = runTest { val transforms = listOf( suspend { delay(3000); "A" }, suspend { delay(2000); "B" }, suspend { delay(4000); "C" }, suspend { delay(1000); "D" }, ) val res = transforms.mapAsync { it() } assertEquals(listOf("A", "B", "C", "D"), res) assertEquals(4000, currentTime) }

But this is not all. We expect a properly implemented suspending function to respect structured concurrency. The easiest way to check this is by specifying some context, like CoroutineName, for the parent coroutine, then check that it is still the same in the transformation function. To capture the context of a suspending function, we can use the currentCoroutineContext function or the coroutineContext property. In lambda expressions nested in coroutine builders or scope functions, we should use the currentCoroutineContext function because the coroutineContext property from CoroutineScope has priority over the property that provides the current coroutine context.

@Test fun `should support context propagation`() = runTest { var ctx: CoroutineContext? = null val name1 = CoroutineName("Name 1") withContext(name1) { listOf("A").mapAsync { ctx = currentCoroutineContext() it } assertEquals(name1, ctx?.get(CoroutineName)) } val name2 = CoroutineName("Some name 2") withContext(name2) { listOf(1, 2, 3).mapAsync { ctx = currentCoroutineContext() it } assertEquals(name2, ctx?.get(CoroutineName)) } }

The easiest way to test cancellation is by capturing the inner function job and verifying its cancellation after the cancellation of the outer coroutine.

@Test fun `should support cancellation`() = runTest { var job: Job? = null val parentJob = launch { listOf("A").mapAsync { job = currentCoroutineContext().job delay(Long.MAX_VALUE) } } delay(1000) parentJob.cancel() assertEquals(true, job?.isCancelled) }

I don’t think such tests are required in most applications, but I find them useful in libraries. It’s not so obvious that structured concurrency is respected. Both the tests above would fail if async were started on an outer scope.

// Incorrect implementation, that would make above tests fail suspend fun <T, R> Iterable<T>.mapAsync( transformation: suspend (T) -> R ): List<R> = this@mapAsync .map { GlobalScope.async { transformation(it) } } .awaitAll()

UnconfinedTestDispatcher

Except for StandardTestDispatcher we also have UnconfinedTestDispatcher. The biggest difference is that StandardTestDispatcher does not invoke any operations until we use its scheduler. UnconfinedTestDispatcher immediately invokes all the operations before the first delay on started coroutines, which is why the code below prints "C".

fun main() { CoroutineScope(StandardTestDispatcher()).launch { print("A") delay(1) print("B") } CoroutineScope(UnconfinedTestDispatcher()).launch { print("C") delay(1) print("D") } } // C

The runTest function was introduced in version 1.6 of kotlinx-coroutines-test. Previously, we used runBlockingTest, whose behavior is much closer to runTest using UnconfinedTestDispatcher. So, if want to directly migrate from runBlockingTest to runTest, this is how our tests might look:

@Test fun testName() = runTest(UnconfinedTestDispatcher()) { //... }

Using mocks

Using delay in fakes is easy but not very explicit. Many developers prefer to call delay in the test function. One way to do this is using mocks3:

@Test fun `should load data concurrently`() = runTest { // given val userRepo = mockk<UserDataRepository>() coEvery { userRepo.getName() } coAnswers { delay(600) aName } coEvery { userRepo.getFriends() } coAnswers { delay(700) someFriends } coEvery { userRepo.getProfile() } coAnswers { delay(800) aProfile } val useCase = FetchUserUseCase(userRepo) // when useCase.fetchUserData() // then assertEquals(800, currentTime) }

In the above example, I’ve used the MockK library.

Testing functions that change a dispatcher

In the Dispatchers chapter, we covered typical cases where we set concrete dispatchers. For example, we use Dispatcher.IO (or a custom dispatcher) for blocking calls, or Dispatchers.Default for CPU-intensive calls. Such functions rarely need to be simultaneous, so typically it is enough to test them with runBlocking. This case is easy and practically indistinguishable from testing blocking functions. For example, think of the following function:

suspend fun readSave(name: String): GameState = withContext(Dispatchers.IO) { reader.readCsvBlocking(name, GameState::class.java) } suspend fun calculateModel() = withContext(Dispatchers.Default) { model.fit( dataset = newTrain, epochs = 10, batchSize = 100, verbose = false ) }

We could test the behavior of such functions in tests wrapped with runBlocking, but how about checking if these functions truly do change the dispatcher? This can also be done if we mock the functions we call, and inside we capture the name of the used thread.

@Test fun `should change dispatcher`() = runBlocking { // given val csvReader = mockk<CsvReader>() val startThreadName = "MyName" var usedThreadName: String? = null every { csvReader.readCsvBlocking( aFileName, GameState::class.java ) } coAnswers { usedThreadName = Thread.currentThread().name aGameState } val saveReader = SaveReader(csvReader) // when withContext(newSingleThreadContext(startThreadName)) { saveReader.readSave(aFileName) } // then assertNotNull(usedThreadName) val expectedPrefix = "DefaultDispatcher-worker-" assert(usedThreadName!!.startsWith(expectedPrefix)) }

In the above function, I couldn't use fakes because CsvReader is a class, so I used mocks.

Remember, that Dispatchers.Default and Dispatchers.IO share the same pool of threads.

However, in rare cases we might want to test time dependencies in functions that do change the dispatcher. This is a tricky case because the new dispatcher replaces our StandardTestDispatcher, so we stop operating in virtual time. To make this clear, let’s wrap the fetchUserData function with withContext(Dispatchers.IO).

suspend fun fetchUserData() = withContext(Dispatchers.IO) { val name = async { userRepo.getName() } val friends = async { userRepo.getFriends() } val profile = async { userRepo.getProfile() } User( name = name.await(), friends = friends.await(), profile = profile.await() ) }

Now all our previously implemented tests will wait in real time, and currentTime will keep returning 0. The easiest way to prevent this is by injecting the dispatcher via a constructor and replacing it in unit tests.

class FetchUserUseCase( private val userRepo: UserDataRepository, private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO ) { suspend fun fetchUserData() = withContext(ioDispatcher) { val name = async { userRepo.getName() } val friends = async { userRepo.getFriends() } val profile = async { userRepo.getProfile() } User( name = name.await(), friends = friends.await(), profile = profile.await() ) } }

Now, instead of providing Dispatchers.IO in unit tests, we should use StandardTestDispatcher from runTest. We can get it from coroutineContext using the ContinuationInterceptor key.

val testDispatcher = this .coroutineContext[ContinuationInterceptor] as CoroutineDispatcher val useCase = FetchUserUseCase( userRepo = userRepo, ioDispatcher = testDispatcher, )

Another possibility is to cast ioDispatcher as CoroutineContext, and replace it in unit tests with EmptyCoroutineContext. The final behavior will be the same: the function will never change the dispatcher.

val useCase = FetchUserUseCase( userRepo = userRepo, ioDispatcher = EmptyCoroutineContext, )

Testing what happens during function execution

Imagine a function which during its execution first shows a progress bar and later hides it.

suspend fun sendUserData() { val userData = database.getUserData() progressBarVisible.value = true userRepository.sendUserData(userData) progressBarVisible.value = false }

If we only check the final result, we cannot verify that the progress bar changed its state during function execution. The trick that is helpful in such cases is to start this function in a new coroutine and control virtual time from outside. Notice that runTest creates a coroutine with the StandardTestDispatcher dispatcher and advances its time until idle (using the advanceUntilIdle function). This means that its children's time will start once the parent starts waiting for them, so once its body is finished. Before that, we can advance virtual time by ourselves.

@Test fun `should show progress bar when sending data`() = runTest { // given val database = FakeDatabase() val vm = UserViewModel(database) // when launch { vm.sendUserData() } // then assertEquals(false, vm.progressBarVisible.value) // when advanceTimeBy(1000) // then assertEquals(false, vm.progressBarVisible.value) // when runCurrent() // then assertEquals(true, vm.progressBarVisible.value) // when advanceUntilIdle() // then assertEquals(false, vm.progressBarVisible.value) }

Notice that, thanks to runCurrent, we can precisely check when some value changes.

A similar effect could be achieved if we used delay. This is like having two independent processes: one is doing things, while the other is checking if the first one is behaving properly.

@Test fun `should show progress bar when sending data`() = runTest { val database = FakeDatabase() val vm = UserViewModel(database) launch { vm.showUserData() } // then assertEquals(false, vm.progressBarVisible.value) delay(1000) assertEquals(true, vm.progressBarVisible.value) delay(1000) assertEquals(false, vm.progressBarVisible.value) }

Using explicit functions like advanceTimeBy is considered more readable than using delay.

Testing functions that launch new coroutines

Coroutines need to start somewhere. On the backend, they are often started by the framework we use (for instance Spring or Ktor), but sometimes we might also need to construct a scope ourselves and launch coroutines on it.

@Scheduled(fixedRate = 5000) fun sendNotifications() { notificationsScope.launch { val notifications = notificationsRepository .notificationsToSend() for (notification in notifications) { launch { notificationsService.send(notification) notificationsRepository .markAsSent(notification.id) } } } }

How can we test sendNotifications if the notifications are truly sent concurrently? Again, in unit tests we need to use StandardTestDispatcher as part of our scope. We should also add some delays to send and markAsSent.

@Test fun testSendNotifications() { // given val notifications = List(100) { Notification(it) } val repo = FakeNotificationsRepository( delayMillis = 200, notifications = notifications, ) val service = FakeNotificationsService( delayMillis = 300, ) val testScope = TestScope() val sender = NotificationsSender( notificationsRepository = repo, notificationsService = service, notificationsScope = testScope ) // when sender.sendNotifications() testScope.advanceUntilIdle() // then all notifications are sent and marked assertEquals( notifications.toSet(), service.notificationsSent.toSet() ) assertEquals( notifications.map { it.id }.toSet(), repo.notificationsMarkedAsSent.toSet() ) // and notifications are sent concurrently assertEquals(700, testScope.currentTime) }

Notice that runBlocking is not needed in the code above. Both sendNotifications and advanceUntilIdle are regular functions.

Replacing the main dispatcher

There is no main function in unit tests. This means that if we try to use it, our tests will fail with the "Module with the Main dispatcher is missing" exception. On the other hand, injecting the Main thread every time would be demanding, so the "kotlinx-coroutines-test" library provides the setMain extension function on Dispatchers instead.

We often define main in a setup function (function with @Before or @BeforeEach) in a base class extended by all unit tests. As a result, we are always sure we can run our coroutines on Dispatchers.Main. We should also reset the main dispatcher to the initial state with Dispatchers.resetMain().

Testing Android functions that launch coroutines

On Android, we typically start coroutines in ViewModels, Presenters, Fragments, or Activities. These are very important classes, and we should test them. Think of the MainViewModel implementation below:

class MainViewModel( private val userRepo: UserRepository, private val newsRepo: NewsRepository, ) : BaseViewModel() { private val _userName: MutableLiveData<String> = MutableLiveData() val userName: LiveData<String> = _userName private val _news: MutableLiveData<List<News>> = MutableLiveData() val news: LiveData<List<News>> = _news private val _progressVisible: MutableLiveData<Boolean> = MutableLiveData() val progressVisible: LiveData<Boolean> = _progressVisible fun onCreate() { viewModelScope.launch { val user = userRepo.getUser() _userName.value = user.name } viewModelScope.launch { _progressVisible.value = true val news = newsRepo.getNews() .sortedByDescending { it.date } _news.value = news _progressVisible.value = false } } }

Instead of viewModelScope, there might be our own scope, and instead of ViewModel, it might be Presenter, Activity, or some other class. It does not matter for our example. As in every class that starts coroutines, we should use StandardTestDispatcher as a part of the scope. Previously, we needed to inject a different scope with a dependency injection, but now there is a simpler way: on Android, we use Dispatchers.Main as the default dispatcher, and we can replace it with StandardTestDispatcher thanks to the Dispatchers.setMain function:

private lateinit var testDispatcher @Before fun setUp() { testDispatcher = StandardTestDispatcher() Dispatchers.setMain(testDispatcher) } @After fun tearDown() { Dispatchers.resetMain() }

After setting the Main dispatcher this way, onCreate coroutines will be running on testDispatcher, so we can control their time. We can use the advanceTimeBy function to pretend that a certain amount of time has passed. We can also use advanceUntilIdle to resume all coroutines until they are done.

class MainViewModelTests { private lateinit var scheduler: TestCoroutineScheduler private lateinit var viewModel: MainViewModel @BeforeEach fun setUp() { scheduler = TestCoroutineScheduler() Dispatchers.setMain(StandardTestDispatcher(scheduler)) viewModel = MainViewModel( userRepo = FakeUserRepository(aName), newsRepo = FakeNewsRepository(someNews) ) } @AfterEach fun tearDown() { Dispatchers.resetMain() viewModel.onCleared() } @Test fun `should show user name and sorted news`() { // when viewModel.onCreate() scheduler.advanceUntilIdle() // then assertEquals(aName, viewModel.userName.value) val someNewsSorted = listOf(News(date1), News(date2), News(date3)) assertEquals(someNewsSorted, viewModel.news.value) } @Test fun `should show progress bar when loading news`() { // given assertEquals(null, viewModel.progressVisible.value) // when viewModel.onCreate() // then assertEquals(false, viewModel.progressVisible.value) // when scheduler.runCurrent() // then assertEquals(true, viewModel.progressVisible.value) // when scheduler.advanceTimeBy(200) // then assertEquals(true, viewModel.progressVisible.value) // when scheduler.runCurrent() // then assertEquals(false, viewModel.progressVisible.value) } @Test fun `user and news are called concurrently`() { // when viewModel.onCreate() scheduler.advanceUntilIdle() // then assertEquals(300, testDispatcher.currentTime) } class FakeUserRepository( private val name: String ) : UserRepository { override suspend fun getUser(): UserData { delay(300) return UserData(name) } } class FakeNewsRepository( private val news: List<News> ) : NewsRepository { override suspend fun getNews(): List<News> { delay(200) return news } } }

Setting a test dispatcher with a rule

JUnit 4 allows us to use rules. These are classes that contain logic that should be invoked on some test class lifecycle events. For instance, a rule can define what to do before and after all tests, therefore it can be used in our case to set our test dispatcher and clean it up later. Here is a good implementation of such a rule:

class MainCoroutineRule : TestWatcher() { lateinit var scheduler: TestCoroutineScheduler private set lateinit var dispatcher: TestDispatcher private set override fun starting(description: Description) { scheduler = TestCoroutineScheduler() dispatcher = StandardTestDispatcher(scheduler) Dispatchers.setMain(dispatcher) } override fun finished(description: Description) { Dispatchers.resetMain() } }

This rule needs to extend TestWatcher, which provides test lifecycle methods, like starting and finished, which we override. It composes TestCoroutineScheduler and TestDispatcher. Before each test in a class using this rule, TestDispatcher will be set as Main. After each test, the Main dispatcher will be reset. We can access the scheduler with the scheduler property of this rule.

class MainViewModelTests { @get:Rule var mainCoroutineRule = MainCoroutineRule() // ... @Test fun `should show user name and sorted news`() { // when viewModel.onCreate() mainCoroutineRule.scheduler.advanceUntilIdle() // then assertEquals(aName, viewModel.userName.value) val someNewsSorted = listOf(News(date1), News(date2), News(date3)) assertEquals(someNewsSorted, viewModel.news.value) } @Test fun `should show progress bar when loading news`() { // given assertEquals(null, viewModel.progressVisible.value) // when viewModel.onCreate() // then assertEquals(true, viewModel.progressVisible.value) // when mainCoroutineRule.scheduler.advanceTimeBy(200) // then assertEquals(false, viewModel.progressVisible.value) } @Test fun `user and news are called concurrently`() { // when viewModel.onCreate() mainCoroutineRule.scheduler.advanceUntilIdle() // then assertEquals(300, mainCoroutineRule.currentTime) } }

If you want to call advanceUntilIdle, advanceTimeBy, runCurrent and currentTime directly on MainCoroutineRule, you can define them as extension functions and properties.

This way of testing Kotlin coroutines is fairly common on Android. It is even explained in Google’s Codelabs materials (Advanced Android in Kotlin 05.3: Testing Coroutines and Jetpack integrations) (currently, for older kotlinx-coroutines-test API).

It is similar with JUnit 5, where we can define an extension:

@ExperimentalCoroutinesApi class MainCoroutineExtension : BeforeEachCallback, AfterEachCallback { lateinit var scheduler: TestCoroutineScheduler private set lateinit var dispatcher: TestDispatcher private set override fun beforeEach(context: ExtensionContext?) { scheduler = TestCoroutineScheduler() dispatcher = StandardTestDispatcher(scheduler) Dispatchers.setMain(dispatcher) } override fun afterEach(context: ExtensionContext?) { Dispatchers.resetMain() } }

Using MainCoroutineExtension is nearly identical to using the MainCoroutineRule rule. The difference is that instead of @get:Rule annotation, we need to use @JvmField and @RegisterExtension.

@JvmField @RegisterExtension var mainCoroutineExtension = MainCoroutineExtension()

Summary

In this chapter, we've discussed the most important use cases for testing Kotlin coroutines. There are some tricks we need to know, but in the end our tests can be very elegant and everything can be tested quite easily. I hope you feel inspired to write good tests in your applications using Kotlin Coroutines.

1:

A fake is a class that implements an interface but contains fixed data and no logic. They are useful to mimic a concrete behavior for testing.

2:

Mocks are universal simulated objects that mimic the behavior of real objects in controlled ways. We generally create them using libraries, like MockK, which support mocking suspending functions. In the examples below, I decided to use fakes to avoid using an external library.

3:

Not everyone likes mocking. On one hand, mocking libraries have plenty of powerful features. On the other hand, think of the following situation: you have thousands of tests, and you change an interface of a repository that is used by all of them. If you use fakes, it is typically enough to update only a few classes. This is a big problem, which is why I generally prefer to use fakes.