What is the right way to test code that uses `Disp...
# kotest
o
What is the right way to test code that uses
Dispatchers.Main
from coroutines under the hood? Environment: I use Kotest 5.6.2 and have enabled coroutineTestScope and timeout = 5.seconds When I call
Dispatchers.setMain
right before test, everything is OK:
Copy code
class MySpec : FeatureSpec({

    val dispatcher = StandardTestDispatcher()

    feature("...") {
        Dispatchers.setMain(dispatcher)

        scenario("...") {
            // It will crash if Main dispatcher was not set before
            launch(Dispatchers.Main) {
                delay(100)
            }.join()

            dispatcher.scheduler shouldBe testCoroutineScheduler // PASSED!
        }

        Dispatchers.resetMain()
    }
})
But when I set Main dispatcher in
beforeTest
or
beforeSpec
, I get `TimeoutCancellationException`:
Copy code
class MySpec : FeatureSpec({

    val dispatcher = StandardTestDispatcher()

    beforeTest { Dispatchers.setMain(dispatcher) }
    afterTest { Dispatchers.resetMain() }

    feature("...") {
        scenario("...") {
            // It will crash if Main dispatcher was not set before
            launch(Dispatchers.Main) {
                delay(100)
            }.join()

            dispatcher.scheduler shouldBe testCoroutineScheduler // PASSED!
        }
    } // CRASH after the test
})
---
Copy code
Timed out waiting for 5000 ms
kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 5000 ms
	at app//kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:184)
    ...
    at io.kotest.engine.test.TestInvocationInterceptor.intercept(TestInvocationInterceptor.kt:31)
	at io.kotest.engine.test.TestCaseExecutor$execute$3$1.invokeSuspend(TestCaseExecutor.kt:97)
    ...
What am I doing wrong?
l
I think it's better using the CoroutineDispatcher provided by the
coroutineContext
inside the lifecycle hooks (or tests):
Copy code
beforeTest { Dispatchers.setMain(coroutineContext[CoroutineDispatcher]!!) }
Although this passes for me as well (note
coroutineTestScope = true
at the beginning of the spec):
Copy code
class MySpec : FeatureSpec({

    coroutineTestScope = true

    val dispatcher = StandardTestDispatcher()

    beforeTest { Dispatchers.setMain(dispatcher) }
    afterTest { Dispatchers.resetMain() }

    feature("...") {
        scenario("...") {
            // It will crash if Main dispatcher was not set before
            launch(Dispatchers.Main) {
                delay(100)
            }.join()
        }
    }
})
o
Sorry, missed your answer. I think the problem exists only if you have specified timeout:
Copy code
class KotestConfig : AbstractProjectConfig() {
    override val coroutineTestScope = true
    override val timeout = 5.seconds
}
But I have no idea why
l
Maybe, I don't specify timeout in my tests.
o
My guess is that if I change Main dispatcher, Kotest uses virtual time not only inside tests but for test timeouts too. So it just skips time until
TimeoutCancellationException
happen.
Interesting. If I specify
invocationTimeout
instead of
timeout
, it works without
TimeoutCancellationException
. The difference I found between these options is that
timeout
applies to test containers, but
invocationTimeout
is not.
👍 1
My guess about virtual time was right. Newer version of kotlinx.coroutines.test prints more clear exception message:
Copy code
Timed out after 5s of _virtual_ (kotlinx.coroutines.test) time. To use the real time, wrap 'withTimeout' in 'withContext(Dispatchers.Default.limitedParallelism(1))'
kotlinx.coroutines.TimeoutCancellationException: Timed out after 5s of _virtual_ (kotlinx.coroutines.test) time. To use the real time, wrap 'withTimeout' in 'withContext(Dispatchers.Default.limitedParallelism(1))'
I’ve filed issue on GitHub: https://github.com/kotest/kotest/issues/3703
👏 1