Testing Kotlin Coroutines

This is a chapter from the book Kotlin Coroutines. You can find Early Access on LeanPub.

Testing suspending functions in most cases is not different from testing normal functions. Take a look at the below showUserData from ShowUserUseCase. Checking if 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 ShowUserUseCase( private val repo: UserDataRepository, private val view: UserDataView ) { suspend fun showUserData() = coroutineScope { val name = async { repo.getName() } val friends = async { repo.getFriends() } val profile = async { repo.getProfile() } val user = User( name = name.await(), friends = friends.await(), profile = profile.await() ) view.show(user) } } class ShowUserDataTest { @Test fun `should show user data on view`() = runBlocking { // given val repo = FakeUserDataRepository() val view = FakeUserDataView() val useCase = ShowUserUseCase(repo, view) // when useCase.showUserData() // then val expectedUser = User( name = "Ben", friends = listOf(Friend("some-friend-id-1")), profile = Profile("Example description") ) assertEquals(listOf(expectedUser), view.showed) } 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") } class FakeUserDataView : UserDataView { var showed = listOf<User>() override fun show(user: User) { showed = showed + user } } } //sampleEnd interface UserDataRepository { suspend fun getName(): String suspend fun getFriends(): List<Friend> suspend fun getProfile(): Profile } interface UserDataView { fun show(user: User) } 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)

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

class UserTests : KtAcademyFacadeTest() { @Test fun `should modify newsletter`() = runBlocking { // given tokenProvider.alwaysReturn(aUserId) facade.addUserFromGoogle(aUserToken, aAddUserRequest) // when facade.updateUserSelf( aUserToken, PatchUserSelfRequest( allowNewsletters = mapOf( KT_ACADEMY to true, LEARNING_DRIVEN to true ) ) ) // then assertEquals( listOf(KT_ACADEMY, LEARNING_DRIVEN), userRepository.findUser(aUserId)?.newsletters ) // when facade.updateUserSelf( aUserToken, PatchUserSelfRequest( allowNewsletters = mapOf( KT_ACADEMY to false ) ) ) // then assertEquals( listOf(LEARNING_DRIVEN), userRepository.findUser(aUserId)?.newsletters ) // when facade.updateUserSelf( aUserToken, PatchUserSelfRequest( allowNewsletters = mapOf( KT_ACADEMY to true, LEARNING_DRIVEN to false ) ) ) // then assertEquals( listOf(KT_ACADEMY), userRepository.findUser(aUserId)?.newsletters ) } @Test fun `should modify user bio`() = test { // given thereIsUser(aUserToken, aUserId) with(userRepository.findUser(aUserId)!!) { assertEquals(null, bio) assertEquals(null, bioPl) assertEquals(null, customImageUrl) } // when facade.updateUserSelf( aUserToken, PatchUserSelfRequest( bio = aUserBio, bioPl = aUserBioPl, publicKey = aUserPublicKey, customImageUrl = aCustomImageUrl ) ) // then with(userRepository.findUser(aUserId)!!) { assertEquals(aUserBio, bio) assertEquals(aUserBioPl, bioPl) assertEquals(aUserPublicKey, publicKey) assertEquals(aCustomImageUrl, customImageUrl) } // when facade.updateUserSelf( aUserToken, PatchUserSelfRequest( bio = "", bioPl = "", publicKey = "", customImageUrl = "", ) ) // then with(userRepository.findUser(aUserId)!!) { assertEquals(null, bio) assertEquals(null, bioPl) assertEquals(null, customImageUrl) } } //... }

Integration tests can be implemented the same way. 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. If we do, we need to update our test functions to simulate that they take a bit more time. We make them take more time by calling delay.

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") } }

Many companies do not test time dependencies - they are only interested in testing if the logic works correctly, not if it works concurrently.

Although actually waiting this time wouldn't be smart - it would make our tests much slower. Instead, we prefer to operate on a simulated time, and just pretend that we waited for it. Here comes kotlinx-coroutines-test for the rescue with its function runBlockingTest. When within its scope we call delay, we don't really wait, but instead, simulated time is modified. We can also check for how much time our coroutines was delayed using the currentTime property.

class TestTest { @Test fun testChecker() = runBlockingTest { assertEquals(0, currentTime) delay(1000) // do not really delay assertEquals(1000, currentTime) } } // this function execution takes milliseconds, // there is no real waiting

Simulated time behaves like a normal time, and concurrent coroutines do not increase it twice.

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

This can be used in our example case, to test if different user data were loaded simultaneously. If they wouldn't, currentTime would be equal to more than 1000. Since each call requires 1000, if they are simultaneous, after loading them all, the currentTime should be 1000. This works, because runBlockingTest provides a scope, whose context is propagated from suspending function to suspending function. When delay is called, it checks if there is used context, that is overriding its behavior. More concretely, it looks for a context with key ContinuationInterceptor that implements Delay interface. Since TestCoroutineScope provided by runBlockingTest is fulfilling these criteria, its fake delay will be used instead of the default one.

// will not start, because runBlockingTest requires kotlinx-coroutines-test, but you can copy it to your project import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.test.runBlockingTest import org.junit.Test import kotlin.test.assertEquals class ShowUserUseCase( private val repo: UserDataRepository, private val view: UserDataView ) { suspend fun showUserData() = coroutineScope { val name = async { repo.getName() } val friends = async { repo.getFriends() } val profile = async { repo.getProfile() } val user = User( name = name.await(), friends = friends.await(), profile = profile.await() ) view.show(user) } } class ShowUserDataTest { //sampleStart @Test fun `should load data concurrently`() = runBlockingTest { // given val repo = FakeUserDataRepository() val view = FakeUserDataView() val useCase = ShowUserUseCase(repo, view) // when useCase.showUserData() // then assertEquals(1000, currentTime) } //sampleEnd @Test fun `should show data on view`() = runBlockingTest { // given val repo = FakeUserDataRepository() val view = FakeUserDataView() val useCase = ShowUserUseCase(repo, view) // when useCase.showUserData() // then val expectedUser = User( name = "Ben", friends = listOf(Friend("some-friend-id-1")), profile = Profile("Example description") ) assertEquals(listOf(expectedUser), view.showed) } 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") } } class FakeUserDataView : UserDataView { var showed = listOf<User>() override fun show(user: User) { showed = showed + user } } } interface UserDataRepository { suspend fun getName(): String suspend fun getFriends(): List<Friend> suspend fun getProfile(): Profile } interface UserDataView { fun show(user: User) } 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)

There is one catch here. This mechanism base on the context with ContinuationInterceptor key. Yes, this is the same key that is used by dispatchers (as mentioned in the chapter dedicated to dispatchers). This is not a very common case, but if we would want to test time dependencies in a function that sets its own dispatcher, our tests would fail (because they would use actual time instead of simulated one). So the below class wouldn't pass the above test.

class ShowUserUseCase { suspend fun showUserData( repo: UserDataRepository, view: UserDataView ) = withContext(Dispatchers.IO) { val name = async { repo.getName() } val friends = async { repo.getFriends() } val profile = async { repo.getProfile() } val user = User( name.await(), friends.await(), profile.await() ) view.show(user) } }

The solution to this problem is that dispatcher should be injected (I suggest constructor injection). Then on the production code, we might use the desired one, and on the tests, we might use EmptyCoroutineContext that does not modify the scope.

// will not start, because runBlockingTest requires kotlinx-coroutines-test, but you can copy it to your project import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.test.runBlockingTest import kotlinx.coroutines.withContext import org.junit.Test import kotlin.coroutines.CoroutineContext import kotlin.coroutines.EmptyCoroutineContext import kotlin.test.assertEquals //sampleStart class ShowUserUseCase( private val repo: UserDataRepository, private val view: UserDataView, private val ioDispatcher: CoroutineContext = Dispatchers.IO ) { suspend fun showUserData() = withContext(ioDispatcher) { val name = async { repo.getName() } val friends = async { repo.getFriends() } val profile = async { repo.getProfile() } val user = User( name = name.await(), friends = friends.await(), profile = profile.await() ) view.show(user) } } class ShowUserDataTest { @Test fun `should load data concurrently`() = runBlockingTest { // given val repo = FakeUserDataRepository() val view = FakeUserDataView() val useCase = ShowUserUseCase( repo, view, EmptyCoroutineContext ) // when useCase.showUserData() // then assertEquals(1000, currentTime) } @Test fun `should show user data on view`() = runBlockingTest { // given val repo = FakeUserDataRepository() val view = FakeUserDataView() val useCase = ShowUserUseCase( repo, view, EmptyCoroutineContext ) // when useCase.showUserData() // then val expectedUser = User( name = "Ben", friends = listOf(Friend("some-friend-id-1")), profile = Profile("Example description") ) assertEquals(listOf(expectedUser), view.showed) } 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") } } class FakeUserDataView : UserDataView { var showed = listOf<User>() override fun show(user: User) { showed = showed + user } } } //sampleEnd interface UserDataRepository { suspend fun getName(): String suspend fun getFriends(): List<Friend> suspend fun getProfile(): Profile } interface UserDataView { fun show(user: User) } 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)

If we truly want, we can still test changing the dispatcher or even used thread name. I believe such tests are generally a bit overzealous, but I would like to show that everything can be tested. In the below example, I show testing that the IO dispatcher is used when user repository methods are called.

import kotlinx.coroutines.* import org.junit.Test import kotlin.coroutines.* import kotlin.test.assertEquals //sampleStart class ShowUserUseCase( private val repo: UserDataRepository, private val view: UserDataView, private val ioDispatcher: CoroutineContext = Dispatchers.IO ) { suspend fun showUserData() = withContext(ioDispatcher) { val name = async { repo.getName() } val friends = async { repo.getFriends() } val profile = async { repo.getProfile() } val user = User( name = name.await(), friends = friends.await(), profile = profile.await() ) view.show(user) } } class ShowUserDataTest { @Test fun `should dispatch to IO thread`() = runBlocking { // given val repo = FakeUserDataRepository() val view = FakeUserDataView() val useCase = ShowUserUseCase(repo, view) // when useCase.showUserData() // then assertEquals( List(3) { Dispatchers.IO }, repo.usedDispatchers ) } class FakeUserDataRepository : UserDataRepository { var usedDispatchers = listOf<ContinuationInterceptor?>() override suspend fun getName(): String { usedDispatchers += coroutineContext[ContinuationInterceptor] return "Ben" } override suspend fun getFriends(): List<Friend> { usedDispatchers += coroutineContext[ContinuationInterceptor] return listOf(Friend("some-friend-id-1")) } override suspend fun getProfile(): Profile { usedDispatchers += coroutineContext[ContinuationInterceptor] return Profile("Example description") } } class FakeUserDataView : UserDataView { var showed = listOf<User>() override fun show(user: User) { showed = showed + user } } } //sampleEnd interface UserDataRepository { suspend fun getName(): String suspend fun getFriends(): List<Friend> suspend fun getProfile(): Profile } interface UserDataView { fun show(user: User) } 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)

The way how these fakes are written, and how tests are designed, should not be used as a reference. There are many conflicting ideas for how tests should look like. In here I am just trying to present the shortest code that is testing what it supposes to, without depending on any external libraries.

Testing what happens after what

Until now, we waited until our function under test is done. Although in some cases, we might want to track what happens after what. Think of the below function, that first shows progress bar and later hides is.

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

To test it, might add 1-second delay to both getUserData and sendUserData. Then on the test, we need to start it in a separate launch to not wait until the function is done. Then, on the test we can control how fake time progresses. The receiver in runBlockingTest lambda expression is of type TestCoroutineScope. It offers us functions to control the time. The most important one is advanceTimeBy. When it is called, our simulated time progresses, and everything that should happen at that time is invoked.

@Test fun `should show progress bar when sending data`() = runBlockingTest { // when launch { useCase.showUserData() } // then assertEquals(false, progressBarVisible.value) advanceTimeBy(1000) assertEquals(true, progressBarVisible.value) advanceTimeBy(1000) assertEquals(false, progressBarVisible.value) }

The same effect could be achieved if we just use delay. It is like having two independent processes - one doing things, and another one checking if the first one behaves properly.

@Test fun `should show progress bar when sending data`() = runBlockingTest { // when launch { useCase.showUserData() } // then assertEquals(false, progressBarVisible.value) delay(1000) assertEquals(true, progressBarVisible.value) delay(1000) assertEquals(false, progressBarVisible.value) }

Replacing main

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

import kotlinx.coroutines.* import kotlinx.coroutines.test.* import org.junit.* import java.util.concurrent.Executors import kotlin.coroutines.ContinuationInterceptor import kotlin.coroutines.CoroutineContext import kotlin.coroutines.coroutineContext import kotlin.test.assertEquals class Data interface MainView { suspend fun show(data: Data) } interface DataRepo { suspend fun fetchData(): Data } //sampleStart class MainPresenter( private val mainView: MainView, private val dataRepository: DataRepo ) { suspend fun onCreate() = coroutineScope { launch(Dispatchers.Main) { val data = dataRepository.fetchData() mainView.show(data) } } } class FakeMainView : MainView { var dispatchersUsedToShow: List<CoroutineContext?> = emptyList() override suspend fun show(data: Data) { dispatchersUsedToShow += coroutineContext[ContinuationInterceptor] } } class FakeDataRepo : DataRepo { override suspend fun fetchData(): Data { delay(1000) return Data() } } class SomeTest { private val mainDispatcher = Executors .newSingleThreadExecutor() .asCoroutineDispatcher() @Before fun setup() { Dispatchers.setMain(mainDispatcher) } @After fun tearDown() { Dispatchers.resetMain() } @Test fun testSomeUI() = runBlocking { // given val view = FakeMainView() val repo = FakeDataRepo() val presenter = MainPresenter(view, repo) // when presenter.onCreate() // then show was called on the main dispatcher assertEquals( listOf(Dispatchers.Main), view.dispatchersUsedToShow ) } } //sampleEnd

Notice, that in the above example in assertEquals I compare dispatchersUsedToShow to the Dispatchers.Main, not to the mainDispatcher. It needs to be this way because mainDispatcher is set as a delegate inside the class provided by Dispatchers.Main.

We often set main on a setup function (function with @Before or @BeforeEach) on a base class extended by all unit tests. Thanks to that, we are always sure, we can run our coroutines on the Dispatchers.Main. We might also reset the main function to the initial state with Dispatchers.resetMain(). This would be especially needed if we have integration tests that need to use the real main dispatcher.

Testing functions that launch a coroutine

One challenging problem is testing non-suspending functions, that start coroutines. Apparently, those functions often need to be tested. For instance in Android, those are often view model functions. Here is an example:

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

Let's assume its scope is constructed based on a simple case that was presented in the chapter Constructing coroutine scope:

abstract class BaseViewModel: ViewModel() { private val context = Dispatchers.Main + SupervisorJob() val scope = CoroutineScope(context) fun onDestroy() { context.cancelChildren() } }

The first problem here is waiting for the coroutines to finish. If we don't do that, our assertions will be executed before the coroutines, and they will not give a correct result. There are a few ways how this problem can be solved. I will start with a very simple solution, that is explicitly waiting for all the children.

val aName = "Some name" val someNews = listOf( News(date3), News(date1), News(date2) ) @Test fun `user and news are shown`() = runBlocking { // given val viewModel = MainPresenter( userRepo = FakeUserRepository(aName), newsRepo = FakeNewsRepository(someNews) ) // when viewModel.onCreate() viewModel.scope .coroutineContext .job .children .forEach { it.join() } // this is needed to await children // then assertEquals(aName, viewModel.userName.value) val someNewsSorted = listOf(News(date1), News(date2), News(date3)) assertEquals(someNewsSorted, viewModel.news.value) }

This complex structure can be easily hidden behind an extension function, like the one below:

suspend fun BaseViewModel.joinChildren() { scope.coroutineContext .job .children .forEach { it.join() } }

Another solution, that you might find on some (especially older) projects, is to replace all the dispatchers with Dispatchers.Unconfined. If everything runs on the same thread, we do not need to explicitly wait for children. This approach is easy, but I find it problematic in some cases since there is no real concurrency if everything runs on the same thread. That is why I prefer explicit children awaiting.

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

Those solutions are good for just function awaiting, but they do not offer testing time dependencies. There is a solution that perfectly fixes this problem. Since it is simple to use, it became nearly a standard in Android development.

In this approach, we need to use TestCoroutineDispatcher. It is the same dispatcher that was used by runBlockingTest, but now we will create it using its constructor.

private val testDispatcher = TestCoroutineDispatcher()

Its context key is ContinuationInterceptor, yes, the same one that is used by dispatchers. So they cannot be used together, because one replaces another.

print(TestCoroutineDispatcher() + Dispatchers.Main) // Dispatchers.Main

This is also a power of the TestCoroutineDispatcher: Since in Android it is a standard to use Dispatchers.Main as a part of the scope we start everything on, we can inject TestCoroutineDispatcher using setMain.

private val testDispatcher = TestCoroutineDispatcher() @Before fun setUp() { Dispatchers.setMain(testDispatcher) }

After that, onCreate coroutines will be running on testDispatcher, so we can control their time. Just as before, we can use advanceTimeBy function to pretend that a certain amount of time passed. We can also use advanceUntilIdle to wait until all coroutines are done.

// will not start, because TestCoroutineDispatcher requires kotlinx-coroutines-test, but you can copy it to your project import kotlinx.coroutines.* import kotlinx.coroutines.test.TestCoroutineDispatcher import kotlinx.coroutines.test.setMain import org.junit.Before import org.junit.Test import java.time.Instant import java.util.* import kotlin.test.assertEquals interface UserRepository { suspend fun getUser(): UserData } interface NewsRepository { suspend fun getNews(): List<News> } data class UserData(val name: String) data class News(val date: Date) interface LiveData<T> { val value: T? } class MutableLiveData<T> : LiveData<T> { override var value: T? = null } abstract class ViewModel() class MainViewModel( private val userRepo: UserRepository, private val newsRepo: NewsRepository ) : BaseViewModel() { private val _userName = MutableLiveData<String>() val userName: LiveData<String> = _userName private val _news = MutableLiveData<List<News>>() val news: LiveData<List<News>> = _news fun onCreate() { scope.launch { val user = userRepo.getUser() _userName.value = user.name } scope.launch { _news.value = newsRepo.getNews() .sortedByDescending { it.date } } } } abstract class BaseViewModel : ViewModel() { private val context = Dispatchers.Main + SupervisorJob() val scope = CoroutineScope(context) fun onDestroy() { context.cancelChildren() } } private val date1 = Date .from(Instant.now().minusSeconds(10)) private val date2 = Date .from(Instant.now().minusSeconds(20)) private val date3 = Date .from(Instant.now().minusSeconds(30)) private val aName = "Some name" private val someNews = listOf(News(date3), News(date1), News(date2)) private val viewModel = MainViewModel( userRepo = FakeUserRepository(aName), newsRepo = FakeNewsRepository(someNews) ) class FakeUserRepository(val name: String) : UserRepository { override suspend fun getUser(): UserData { delay(1000) return UserData(name) } } class FakeNewsRepository(val news: List<News>) : NewsRepository { override suspend fun getNews(): List<News> { delay(1000) return news } } //sampleStart class MainViewModelTests { private val testDispatcher = TestCoroutineDispatcher() @Before fun setUp() { Dispatchers.setMain(testDispatcher) } @Test fun `user name is shown`() { // when viewModel.onCreate() // then testDispatcher.advanceTimeBy(1000) assertEquals(aName, viewModel.userName.value) } @Test fun `sorted news are shown`() { // when viewModel.onCreate() // then testDispatcher.advanceTimeBy(1000) val someNewsSorted = listOf(News(date1), News(date2), News(date3)) assertEquals(someNewsSorted, viewModel.news.value) } @Test fun `user and news are called concurrently`() { // when viewModel.onCreate() testDispatcher.advanceUntilIdle() // then assertEquals(1000, testDispatcher.currentTime) } } //sampleEnd

Setting test dispatcher with a rule

JUnit since version 4.7 allows us to use rules. Those are classes that contain a logic, that should be invoked on some test class lifecycle events. For instance, it can be defined to do something before and after all tests. So it can be used in our case to set test dispatcher and to later clean it up. Here is a good implementation of this rule:

class MainCoroutineRule( val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() ) : TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) { override fun starting(description: Description) { super.starting(description) Dispatchers.setMain(dispatcher) } override fun finished(description: Description) { super.finished(description) cleanupTestCoroutines() Dispatchers.resetMain() } }

The rule needs to extend TestWatcher, which provides test lifecycle methods, like starting and finished that are overridden here. We also made it implement TestCoroutineScope and delegated this interface to object TestCoroutineScope(dispatcher) (this is interface delegation feature in action). Thanks to that, we can use all the TestCoroutineScope features, including controlling time (this is the same class that is used by runBlockingTest). This is how unit tests can be implemented if we use this rule:

// will not start, because TestCoroutineDispatcher requires kotlinx-coroutines-test, but you can copy it to your project import kotlinx.coroutines.* import kotlinx.coroutines.test.* import org.junit.Rule import org.junit.Test import org.junit.rules.TestWatcher import org.junit.runner.Description import java.time.Instant import java.util.* import kotlin.test.assertEquals interface UserRepository { suspend fun getUser(): UserData } interface NewsRepository { suspend fun getNews(): List<News> } data class UserData(val name: String) data class News(val date: Date) interface LiveData<T> { val value: T? } class MutableLiveData<T> : LiveData<T> { override var value: T? = null } abstract class ViewModel() class MainViewModel( private val userRepo: UserRepository, private val newsRepo: NewsRepository ) : BaseViewModel() { private val _userName = MutableLiveData<String>() val userName: LiveData<String> = _userName private val _news = MutableLiveData<List<News>>() val news: LiveData<List<News>> = _news fun onCreate() { scope.launch { val user = userRepo.getUser() _userName.value = user.name } scope.launch { _news.value = newsRepo.getNews() .sortedByDescending { it.date } } } } abstract class BaseViewModel : ViewModel() { private val context = Dispatchers.Main + SupervisorJob() val scope = CoroutineScope(context) fun onDestroy() { context.cancelChildren() } } class MainCoroutineRule( val dispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher() ) : TestWatcher(), TestCoroutineScope by TestCoroutineScope(dispatcher) { override fun starting(description: Description) { super.starting(description) Dispatchers.setMain(dispatcher) } override fun finished(description: Description) { super.finished(description) cleanupTestCoroutines() Dispatchers.resetMain() } } private val date1 = Date.from(Instant.now().minusSeconds(10)) private val date2 = Date.from(Instant.now().minusSeconds(20)) private val date3 = Date.from(Instant.now().minusSeconds(30)) val aName = "Some name" val someNews = listOf(News(date3), News(date1), News(date2)) class FakeUserRepository(val name: String) : UserRepository { override suspend fun getUser(): UserData { delay(1000) return UserData(name) } } class FakeNewsRepository(val news: List<News>) : NewsRepository { override suspend fun getNews(): List<News> { delay(1000) return news } } //sampleStart class MainViewModelTests { @get:Rule var mainCoroutineRule = MainCoroutineRule() val viewModel = MainViewModel( userRepo = FakeUserRepository(aName), newsRepo = FakeNewsRepository(someNews) ) @Test fun `user name is shown`() { // when viewModel.onCreate() // then mainCoroutineRule.advanceTimeBy(1000) assertEquals(aName, viewModel.userName.value) } @Test fun `sorted news are shown`() { // when viewModel.onCreate() // then mainCoroutineRule.advanceTimeBy(1000) val someNewsSorted = listOf(News(date1), News(date2), News(date3)) assertEquals(someNewsSorted, viewModel.news.value) } @Test fun `user and news are called concurrently`() { // when viewModel.onCreate() mainCoroutineRule.advanceUntilIdle() // then assertEquals(1000, mainCoroutineRule.currentTime) } } //sampleEnd

Such way to test Kotlin coroutines seems to be quite common in Android. It is even explained on Codelabs materials from Google (Advanced Android in Kotlin 05.3: Testing Coroutines and Jetpack integrations). I hope you see, that the final result is really powerful and easy to use.

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 really elegant and everything can be tested quite easily. I hope it will inspire you 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 for testing, to mimic a concrete behavior.

2:

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