runTest over runBlocking
Sanny Segue (Kyaw Swar Than)
Lead Software Engineer @ Dyson | Formerly @ Grab
During a recent code review, I got curious?why runTest over runBlocking while both execute suspending code block in synchronise behaviour.
Time advancing
One major benefit is the ability to skip delay and advance the time to future. Imaging running the following code with runBlocking, I bet you'll ever see this in your production code but bare with me and this following unit test block would take 10 second to complete. ??
@Test
fun `demonstrate virtual time advancement`() = runBlocking {
var counter = 0
launch {
delay(10_000) // 10 seconds delay
counter++
}
// Test completes after 10-second delay
assertEquals(1, counter)
}
However, with a slight change from runBlocking to runTest, the same test now completes instantly.
@Test
fun `demonstrate virtual time advancement`() = runTest {
...
}
That is because runTest use special test dispatcher to intercept the delay and return continuation with virtual timestamp. [source]
TestDispatcher
Let's slightly tweak the previous code and add custom dispatcher.
The output produce the result similar to runBlocking that the test complete with 10-second delay even with runTest . By default, runTest CANNOT skip delays on real dispatchers (like Dispatchers.IO) in child coroutines.
@Test
fun `demonstrate virtual time advancement`() = runTest {
....
launch(Dispatchers.IO) { // ?? Real dispatcher
delay(10_000) // 10 seconds delay
counter++
}
...
}
To skip delays, one option is to inject special test dispatcher which runTest internally use.
val testDispatcher = StandardTestDispatcher(testScheduler)
launch(testDispatcher) { // ? Test dispatcher
delay(10_000) // 10 seconds delay
...
}
Hence one of the best practices is to always look out for an opportunity to inject dispatcher for one's customViewModel class instead of directly launching coroutine with explicit dispatcher.