Update 2021-12-27
runBlockingTest
was deprected in kotlinx.coroutines version 1.6.0, and will be removed in 1.8.0. The replacement runTest
doesn’t suffer from the issue described in this post.
runBlockingTest
has quite a few pitfalls. One of them is this obscure exception:
java.lang.IllegalStateException: This job has not completed yet
at kotlinx.coroutines.JobSupport.getCompletionExceptionOrNull(JobSupport.kt:1189)
at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest(TestBuilders.kt:53)
at kotlinx.coroutines.test.TestBuildersKt.runBlockingTest$default(TestBuilders.kt:45)
tl;dr: runBlockingTest
doesn’t execute the test body until it’s completed, but only until the test-dispatcher is idle. The exception is thrown when the test body in runBlockingTest
suspends and the dispatcher doesn’t know when it will resume.
That can happen if suspend funs
are called in the test that have to wait for something from outside the test body.
Try to avoid such suspend funs
within runBlockingTest
.
Maybe you can work around that with Fake implementations.
If nothing else works there is still the option to use runBlocking
.
Cause of the exception
We can cause the exception like this:
suspend fun suspendAndNeverResume() = suspendCoroutine<Unit> { continuation ->
// We never call `continuation.resume(Unit)` here.
}
@Test
fun `suspend but never resume in runBlockingTest`() = runBlockingTest {
suspendAndNeverResume()
}
or also like this:
@Test
fun `CompletableDeferred that is never completed`() {
val neverCompleted = CompletableDeferred<Unit>()
runBlockingTest {
neverCompleted.await()
}
}
All these example tests will not suspend forever, but instead will fail immediately with IllegalStateException: This job has not completed yet
.
To understand why it’s happening, we can peek into the runBlockingTest
implementation:
public fun runBlockingTest(context: CoroutineContext = EmptyCoroutineContext, testBody: suspend TestCoroutineScope.() -> Unit) {
…
val deferred = scope.async {
scope.testBody()
}
dispatcher.advanceUntilIdle()
deferred.getCompletionExceptionOrNull()?.let {
throw it
}
…
}
We can see that testBody
gets executed in an async
block. The returned Deferred (=Promise) is stored in deferred
.
Instead of waiting for the testBody
to finish with deferred.await()
, the implementation only advances the dispatcher until there is nothing left to do immediately by calling dispatcher.advanceUntilIdle()
.
Functions like delay
tell the dispatcher immediately when they need to resume.
When there are multiple coroutines active in a test they usually resume each other.
All resumes add a new task for the dispatcher, so it’s usually never idle before the test is done.
That’s why we don’t see this exception very often.
The next statement is not deferred.await()
to wait for the testBody
to finish, but deferred.getCompletionExceptionOrNull()
.
It re-throws the exception that was thrown within the testBody
if there was any.
But in our case, there was no exception and the testBody
isn’t completed yet.
If we peek into the implementation of getCompletionExceptionOrNull
as well, we see that the exception that we’re getting is thrown there:
public fun getCompletionExceptionOrNull(): Throwable? {
val state = this.state
check(state !is Incomplete) { "This job has not completed yet" }
return state.exceptionOrNull
}
Conclusion
runBlockingTest
doesn’t wait for the testBody
to finish but waits for an idle dispatcher.
That’s why we’re getting the IllegalStateException: This job has not completed yet
when the test body suspends but there is no resume scheduled in the dispatcher yet.
It avoids that tests are taking too long or hang forever when the resume never comes.
That can be annoying especially within Instrumentation Tests.
There, we sometimes really have to wait for some Android stuff to happen until we can resume the test.
In that case, you can fall back on runBlocking
. But be aware that runBlocking
might hang forever if the function really doesn’t resume and it also doesn’t come with eager execution by default.