kotlinx.coroutines
kotlinx.coroutines copied to clipboard
Test scheduler does not work when using `delay` inside `runBlocking` on a `runTest`
I'm trying to write a test for a blocking function that uses coroutines internally, wrapped by a runBlocking call. the basic flow would be represented by something like:
@Test
fun schedulerOnRunBlocking() = runTest {
val delayTime = measureTime {
delay(100)
}
println(delayTime)
runBlocking {
val delayInsideRunBlockingTime = measureTime {
delay(100)
}
println(delayInsideRunBlockingTime)
}
}
Surprisingly enough, the output of this test is:
2.632949ms
100.145498ms
I even tried some crazy things such as:
@Test
fun schedulerOnRunBlocking2() = runTest {
val delayTime = measureTime {
delay(100)
}
println(delayTime)
val testContext: CoroutineContext = StandardTestDispatcher(testScheduler)
val singleThreadDispatcher: CoroutineContext = newSingleThreadContext("c1")
runBlocking {
val delayInsideRunBlockingTime = measureTime {
withContext(testContext + singleThreadDispatcher) {
delay(100)
}
}
println(delayInsideRunBlockingTime)
}
}
but this doesn't work, I guess because singleThreadDispatcher overrides the logic of the StandardTestDispatcher that make delays skip real time.
Also, I couldn't find any workaround either. I could pass some custom scope or context to my function I'm trying to test, but I can´t seem to make delay not actually delay on tests.
Hi! Please use the Kotlin Slack or Stack Overflow for questions about usage. We try to reserve this place for issues for us to solve specifically.
First and foremost, runTest launches a coroutine in which the test happens. The documentation for runBlocking (https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) states this:
This function should not be used from a coroutine.
So, what you're trying to do is completely unsupported due to the nature of runBlocking itself. Instead of testing the invocation of runBlocking, it's more reliable to test the coroutine that is wrapped in runBlocking.
Hi! Please use the Kotlin Slack or Stack Overflow for questions about usage. We try to reserve this place for issues for us to solve specifically.
This was not a question about usage, I really thought that delays inside runBlocking should use the custom testScheduler and therefore we had a bug.
First and foremost,
runTestlaunches a coroutine in which the test happens. The documentation forrunBlocking(https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html) states this:This function should not be used from a coroutine.
Yes, you're right, but this is not easy to realise. With the runBlocking explicitly written in the test it might seem obvious, but in reality runBlocking is deep down the method I'm trying to test. it took me some time to understand and narrow down the situation to the example above.
it's more reliable to test the coroutine that is wrapped in
runBlocking.
This is not possible since the code inside runBlocking is not public, therefore not testable. I'd have to compromise my design and maybe expose private behaviour as internal so I could test, not ideal.
So, what you're trying to do is completely unsupported due to the nature of
runBlockingitself. Instead of testing the invocation ofrunBlocking
With this being unsupported, I think we are basically establishing that we can not test blocking methods (that may use runBlocking internally) with runTest, which I guess is reasonable. We might just need to state it more clearly in the docs (if not already).
A rule of thumb is, if you call a suspend function from the runTest block, the delays in that function will be skipped, but if not, it's just a blocking call that may or may not use coroutines internally, it's just its implementation detail, so runTest has no control over that.
This is not possible since the code inside
runBlockingis not public, therefore not testable.
But that code can be internal, and so testable even without exposing it to the code that uses your library. This is fairly common. Do you see any problems with this approach?
Most code that uses coroutine is structured in one of two ways:
- exposing
suspendfunctions (can be tested with delay skipping just by calling them), or - exposing non-
suspendfunctions that do something likescope.launch { ... }inside of them, wherescopeis mockable (can be tested with delay skipping by mockingscopewithTestScope).
runBlocking is useful fairly situationally, typically for implementing existing interfaces that expect their members to be normal blocking functions.