With the older coroutines API, I used to use the f...
# coroutines
a
With the older coroutines API, I used to use the following utility when I was running some unit tests involving coroutines:
Copy code
fun asyncTest(
    context: CoroutineContext = EmptyCoroutineContext, // in practice, it was never changed
    timeoutInMillis: Long = 15000,
    block: suspend CoroutineScope.() -> Unit
) {
    runBlocking(context) {
        withTimeout(timeoutInMillis, block)
    }
}
It was supposed to fail the test if it took unexpectedly long to execute (real time, not virtual time) to avoid blocking the build agent in the CI system. Now, after the migration to coroutines 1.6.x, I wanted to additionally use the new
runTest
function that additionally checks if there are no active coroutines when the test is finished. Therefore I’m wondering if something like this is a good enough replacement for my previous utility:
Copy code
fun asyncTest(
    context: CoroutineContext = UnconfinedTestDispatcher(),
    timeoutInMillis: Long = 15000,
    block: suspend TestScope.() -> Unit
) = runBlocking {
    withTimeout(timeoutInMillis) {
        runTest(context, timeoutInMillis, block)
    }
}
// EDIT: Nope, it doesn’t work the way I expected. It doesn’t work in the same way as my previous utility, e.g. when there is a coroutine with an endless while-loop with a delay inside it. Any ideas?
j
Why not just use
runTest
directly, instead of wrapping in
runBlocking
and
withTimeout
? I think
runTest
gives you those behaviors already. Otherwise, the only reason for keeping
asyncTest
would be the non-standard default arguments you provide.
a
Because `runTest`’s timeout works differently than what I wanted to achieve.
In the general case, if there are active jobs, it’s impossible to detect if they are going to complete eventually due to the asynchronous nature of coroutines. In order to prevent tests hanging in this scenario,
runTest
will wait for
dispatchTimeoutMs
milliseconds (by default, 60 seconds) from the moment when
TestCoroutineScheduler
becomes idle before throwing
AssertionError
. If some dispatcher linked to
TestCoroutineScheduler
receives a task during that time, the timer gets reset.
I had a case when I was accidentally looping forever in a coroutine and the `runTest`’s timeout didn’t fail that test.
I guess my idea was wrong and it doesn’t work the same way it did before 😞 This works with my old implementation:
Copy code
@Test(expected = TimeoutCancellationException::class)
fun `check that running coroutine fails the test`() = asyncTest(timeoutInMillis = 5000L) {
    val job = launch {
        while (true) {
            delay(1000)
        }
    }
    job.join()
}
but when I use the new one (with
runTest
and
UnconfinedTestDisptatcher
), it hangs forever.
j
Oh, I see what you're saying now...
I've misunderstood how
dispatchTimeoutMs
behaves. It's not a substitute for
withTimeout
, after all.
a
Yep
j
Given
...runTest will wait for dispatchTimeoutMs milliseconds (by default, 60 seconds) from the moment when TestCoroutineScheduler becomes idle before throwing AssertionError.
Then my next question was - So, how does the dispatcher become idle? Looks like changing dispatcher will do that. May not help you, but thought I'd share... Here
dispatchTimeoutMs
behaves like
withTimeout
. On the downside, no time-savings from using the
TestCoroutineScheduler
.
Copy code
runTest(dispatchTimeoutMs = 1000) {
    launch(<http://Dispatchers.IO|Dispatchers.IO>) {
        while (true) {
            delay(5000)
        }
    }
}
a
Yeah, it’s either the “savings” or the “real-world” timeout. I tried to figure out how to have the best of both worlds 🙂
👍 1