runTest vs runBlocking - Simplified
As the name runBlocking
suggests that it runs suspending code in blocking manner, which means if there is a delay inside the unlocking block, it will also block the thread.
fun main(){
runBlocking{
delay(2000)
println("Inside runBlocking")
}
println("Outside run")
}
Inside runBlocking
will be printed first and Outside run
will be printed later. Just like it’s all in a sequence.
Okay, let’s look at this piece of code.
fun main(){
runBlocking{
launch{
delay(2000)
println("First launch")
}
delay(1000)
println("Inside runBlocking")
}
println("Outside run")
}
Here, code after runBlocking
will be executed only when runBlocking is finished.
So the order of output would be:
Inside runBlocking
First launch
Outside run
runBlocking
is not suitable for production code because you don’t want to block the underlying thread.
But it can be used for test cases because we don’t want the code to run in parallel and yield results at different points in time, which would make the testing hard, we simplify it by running in a sequence so proper assertions can be made.
Let’s move onto runTest
It’s just like runBlocking
, it will block the underlying thread,
but it will automatically skip the delays using a special Dispatcher (explained later) and gives you more control over the coroutine.
👉
runTest
is offered by Coroutine Testing Library. More here
Let’s write code to test this function:
suspend fun foo() : Boolean {
delay(5000)
//some operations
return true
}
You can write test, that will skip the delay and immediately return the results by using runTest
class TestExample{
@Test
fun fooTest() = runTest{
val result = foo()
assertk.assertThat(result).isTrue() //Assertk is cool
}
}
The above test will always run without any delay.
Now take a look at the following function
suspend fun doo(): Boolean{
return withContext(Dispatchers.Default){
delay(5000)
true
}
}
What do you think will happen if we write the code to test it, just like above?
The test function will not automatically skip the delay, because, as I mentioned above, runTest
uses a special Dispatcher, and if the delay block is not in the same dispatcher, it will not be skipped, here the delay block is in Default dispatcher, which runTest
has no control over.
How to fix such a situation?
Use the same dispatcher being used by runTest
by passing it to the function being tested as an argument.
Here is how to get the dispatcher.
@OptIn(ExperimentalStdlibApi::class)
@Test
fun testDoo() = runTest {
val dispatcher = coroutineContext[CoroutineDispatcher]
val result = doo(dispatcher!!)
assertk.assertThat(result).isTrue()
}
Now, it will shine.
Keep Coding
Subscribe to my newsletter
Read articles from Waqas Younis directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Waqas Younis
Waqas Younis
Indie Mobile app developer