13
Testing Coroutines
Written by Luka Kordić
Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as
text.You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.
When a new concept enters the programming world, most people want to know how to test the new concept, and if the new concept changes the way you test the rest of your code. Testing asynchronous code to make sure it runs and functions correctly is a good thing. Naturally, people started asking how do you test coroutines, when it’s such a different mechanism, compared to what used to be used in the JVM world.
The process of testing code is usually tied with writing Unit and Integration tests. This is code which you can run fast, debug, and use to confirm that a piece of software you’ve written is still working properly after you apply some changes to the code. Or rather that it doesn’t work, because you’ve changed it, and now you should update the tests to reflect all the remaining cases.
Unit tests run a single unit of code, which should be as small as possible - like a small function’s input and output. Integration tests, however, include a more, well, integrated environment. They usually test multiple classes working together. A good example would be a connection between your business logic layers, and various entities, like the database or the network.
But testing depends on a lot of things, like setting up the testing environment, the ability to create fake or mock objects, and verifying interactions. Let’s see how to do some of those things with coroutines.
Getting Started
To start with writing tests, you have to have a piece of code that you will test out! However, there is something known as TDD - Test Driven Development where tests are usually written first followed by the code. Open up this chapter’s folder, named testing-coroutines and find the starter project. Import the project, and you can explore the code and the project structure.
First, within the contextProvider folder, you have the CoroutineContextProvider
, and its implementation. This is a vital part of the testing setup because you’ll use this to provide a test CoroutineContext
, for your coroutines.
class CoroutineContextProviderImpl(
private val context: CoroutineContext
) : CoroutineContextProvider {
override fun context(): CoroutineContext = context
}
Next, the model package simply holds the User
which you’ll fetch and display in code, and use to test if the code is working properly.
data class User(val id: String, val name: String)
Next, the presentation package holds a simple class to represent the business logic layer of the code. You’ll use MainPresenter.kt to imitate the fetching of a piece of data.
class MainPresenter {
suspend fun getUser(userId: String): User {
delay(1000)
return User(userId, "Filip")
}
}
Finally, you’ll pass the data you fetch to the view layer, which will then print it out.
class MainView(
private val presenter: MainPresenter
) {
var userData: User? = null
fun fetchUserData() {
GlobalScope.launch(Dispatchers.IO) {
userData = presenter.getUser("101")
}
}
fun printUserData() {
println(userData)
}
}
One last thing you have to add, to be able to test code and coroutines, are the Gradle dependencies. If you open up build.gradle, you’ll see these two lines of code:
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test: $kotlin_coroutines_version"
testImplementation 'junit:junit:4.13.2'
The former introduces helper functions and classes, specifically to test coroutines, and the latter gives you the ability to use the JUnit4 testing framework, for the JVM.
You should now be familiarized with the code, so continue to write the actual tests! :]
Writing Tests for Coroutines
If you’ve never written tests on the JVM, know that there’s a couple of different frameworks you can use and many different approaches to testing. For the sake of simplicity, you’ll write a simple, value-asserting test, using the JUnit4 framework for the JVM test suite.
package view
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import presentation.MainPresenter
import kotlinx.coroutines.ExperimentalCoroutinesApi
@OptIn(ExperimentalCoroutinesApi::class)
class MainViewTest {
private val mainPresenter by lazy { MainPresenter() }
private val mainView by lazy { MainView(mainPresenter) }
@Test
fun testFetchUserData() {
// todo add test code
}
}
@Test
fun testFetchUserData() {
// initial state
assertNull(mainView.userData)
// updating the state
mainView.fetchUserData()
// checking the new state, and printing it out
assertEquals("Filip", mainView.userData?.name)
mainView.printUserData()
}
java.lang.AssertionError:
Expected :Filip
Actual :null
<Click to see difference>
Setting Up the Test Environment
The problem you’re facing when running the test is because of the way coroutines and test environments work internally. Because you’re hardcoding the MainPresenter
and MainView
calls to the GlobalScope
and Dispatchers.IO
, you’re losing the ability for the test JVM environment to adapt to coroutines.
Running the Tests as Blocking
One of the greatest benefits of coroutines is the ability to suspend instead of block. This proves to be a powerful mechanism, which allows for things like simple context switching and thread synchronization, parallelism and much more.
/**
* Executes [testBody] as a test in a new coroutine, returning [TestResult].
*/
@ExperimentalCoroutinesApi
public fun runTest(
context: CoroutineContext = EmptyCoroutineContext,
dispatchTimeoutMs: Long = DEFAULT_DISPATCH_TIMEOUT_MS,
testBody: suspend TestScope.() -> Unit
): TestResult
@Test
fun exampleTest() = runTest {
val deferred = async {
delay(1_000)
async {
delay(1_000)
}.await()
}
deferred.await() // result available immediately
}
Using Test CoroutineScope and CoroutineContext
To start using runTest
, you have to integrate the rest of the test environment for coroutines. Two things will ultimately help you control and affect the coroutines and other suspending functions within your test code. The TestScope
and its context. Add the following declarations above your testFetchUserData
method:
// 1
private val testCoroutineDispatcher = StandardTestDispatcher()
// 2
private val testCoroutineScope =
TestScope(testCoroutineDispatcher)
class MainView(
private val presenter: MainPresenter,
private val contextProvider: CoroutineContextProvider,
private val coroutineScope: CoroutineScope
) {
var userData: User? = null
fun fetchUserData() {
coroutineScope.launch(contextProvider.context()) {
userData = presenter.getUser("101")
}
}
fun printUserData() {
println(userData)
}
}
// 1
private val testCoroutineDispatcher = StandardTestDispatcher()
// 2
private val testCoroutineScope = TestScope(testCoroutineDispatcher)
// 3
private val testCoroutineContextProvider =
CoroutineContextProviderImpl(testCoroutineDispatcher)
// 4
private val mainPresenter by lazy { MainPresenter() }
private val mainView by lazy {
MainView(
mainPresenter,
testCoroutineContextProvider,
testCoroutineScope
)
}
@Test
fun testFetchUserData(): Unit = testCoroutineScope.runTest {
assertNull(mainView.userData)
mainView.fetchUserData()
assertEquals("Filip", mainView.userData?.name)
mainView.printUserData()
}
Advancing Time
When you delay a coroutine, you’re effectively stating how long it will wait until it resumes again. If you want to skip the wait, all you have to do, within a coroutine, is to advance the time by the same amount you’re delaying.
@Test
fun testFetchUserData() = testCoroutineScope.runTest {
assertNull(mainView.userData)
mainView.fetchUserData()
// advance the test clock
advanceTimeBy(1010)
assertEquals("Filip", mainView.userData?.name)
mainView.printUserData()
}
Summing it up
Testing coroutines may not be completely straightforward as it is with regular code which uses callbacks, or blocking calls, but there’s a lot of documentation available, and it’s fairly easy to set up. To learn more about the test coroutine helpers and classes, check out the official documentation at the following link: https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test.