In the `kotlinx.coroutines.test` migration guide f...
# coroutines
s
In the
kotlinx.coroutines.test
migration guide for 1.6+ when talking about replacing
runBlockingTest
with
runTest
there is a little line:
Copy code
It works properly with other dispatchers and asynchronous completions.
No action on your part is required, other than replacing runBlocking with runTest as well.
What is meant by this? Why does
runBlocking
need to be replaced with
runTest
if there is no dispatcher/virtual time manipulation?
d
If you're not relying on virtual time, you can stick with
runBlocking
, it's just a bit less convenient: • There's no
runBlocking
on JS, so the test won't be usable in multiplatform code. This was the message behind the line: 1.6 introduced multiplatform coroutine testing. •
runBlocking
returns the value that its block returns. JUnit, for example, expects the test functions to return
Unit
. So, something like
@Test fun test() = runBlocking { assertFailsWith<IllegalArgumentException> { throw IllegalArgumentException() } }
will, surprisingly, not be executed by JUnit, because
test()
returns an
IllegalArgumentException
. •
runTest
is purely for tests, which means we can enhance it with other niceties that have no place in production code. For example, we recently added a way to cancel some of the coroutines in a test when the test body finishes: https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-test/kotlinx.coroutines.test/-test-scope/background-scope.html You won't have that with
runBlocking
.
🙏 1
1
o
One limitation of the new
runTest
implementation is that you cannot run multithreaded (stress) tests with a standard dispatcher. For example:
runTest(Dispatchers.Default) { ... }
produces
IllegalArgumentException: Dispatcher must implement TestDispatcher: Dispatchers.Default
.
d
Yeah, it can be inconvenient, though not really a hard limitation per se. One can still do
runTest { launch(Dispatchers.Default) { ... } }
or
runTest { withContext(Dispatchers.Default) { ... } }
.
🙏 1
o
Ah, good to know, thanks! Could changing to a non-
TestDispatcher
(as shown) conflict with some implementation dependency as
runTest
internals evolve over time? If not, would you consider making
runTest
accept a non-
TestDispatcher
directly and document that behavior?
d
Not sure what dependencies you're talking about,
kotlinx-coroutines-test
is made entirely in-house. However, I think it would be misleading API-wise to allow passing a non-
TestDispatcher
to
runTest
, because then the functions provided by
TestScope
(in which
runTest
runs its blocks) don't make sense if the dispatcher that runs the test body is not integrated with the test module. By forcing the framework users to do
withContext
manually, we make this explicit: hey, you're no longer in the
TestScope
and your code does not play by the same rules. I would write a wrapper such as this if you need such behavior often:
Copy code
@OptIn(ExperimentalStdlibApi::class)
public fun runStressTest(
    context: CoroutineContext = Dispatchers.Default,
    testBody: suspend CoroutineScope.() -> Unit
): TestResult = runTest(context.minusKey(CoroutineDispatcher), dispatchTimeoutMs = Long.MAX_VALUE) {
    withContext(context, testBody)
}
o
That's the kind of wrapper I am using currently. By dependency I did not mean one in the Gradle sense (artifact), but simply my code depending on some specific way
runTest
is implemented, which currently works if I switch to another dispatcher but might not work in a future version.
And the idea would be that even if I'm using a non-test dispatcher,
runTest
could possibly check for leaking coroutines at the end. I did not look into specifics, though.
d
Since 1.6, the test dispatcher only provides that, the dispatching behavior. Catching exceptions and leaked coroutines are handled separately. The intention is to keep it this way, exactly to allow the choice of a dispatcher not to affect a lot.
👍 1