kotlinx.coroutines icon indicating copy to clipboard operation
kotlinx.coroutines copied to clipboard

Test scheduler does not work when using `delay` inside `runBlocking` on a `runTest`

Open bcmedeiros opened this issue 3 years ago • 1 comments
trafficstars

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.

bcmedeiros avatar Sep 20 '22 18:09 bcmedeiros

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.

dkhalanskyjb avatar Sep 21 '22 10:09 dkhalanskyjb

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, 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.

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 runBlocking itself. Instead of testing the invocation of runBlocking

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).

bcmedeiros avatar Sep 22 '22 02:09 bcmedeiros

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 runBlocking is 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 suspend functions (can be tested with delay skipping just by calling them), or
  • exposing non-suspend functions that do something like scope.launch { ... } inside of them, where scope is mockable (can be tested with delay skipping by mocking scope with TestScope).

runBlocking is useful fairly situationally, typically for implementing existing interfaces that expect their members to be normal blocking functions.

dkhalanskyjb avatar Sep 23 '22 08:09 dkhalanskyjb