runTest vs runBlocking - Simplified

Waqas YounisWaqas Younis
2 min read

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

0
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