ursus
08/21/2025, 7:38 PMtestApplication
at test time?
class FooTest {
...
private val scheduler = TestCoroutineScheduler()
private val standardTestDispatcher = StandardTestDispatcher(scheduler)
private val dispatcherProvider = TestDispatcherProvider(standardTestDispatcher) <---------
private val appDatabase = InMemoryDatabase(...)
private val fooDao = FooDao(dispatcherProvider, appDatabase.fooQueries) <-----------
@Test
fun foo() = testApplication(standardTestDispatcher) { <---------
turbineScope {
val fooTurbine = fooDao.foo.testIn(this)
assertThat(fooTurbine.awaitItem()).isNull()
fooDao.saveFoo(..)
assertThat(fooTurbine.awaitItem()).isNotNull()
fooTurbine.cancelAndEnsureAllEventsConsumed()
}
}
}
class TestDispatcherProvider(standardTestDispatcher: TestDispatcher) : DispatcherProvider {
override val io: CoroutineDispatcher = standardTestDispatcher
override val main: CoroutineDispatcher = standardTestDispatcher
override val default: CoroutineDispatcher = standardTestDispatcher
}
crashes with
java.lang.IllegalStateException: Detected use of different schedulers. If you need to use several test coroutine dispatchers, create one `TestCoroutineScheduler` and pass it to each of them.
at kotlinx.coroutines.test.TestCoroutineSchedulerKt.checkSchedulerInContext(TestCoroutineScheduler.kt:257)
at kotlinx.coroutines.test.TestCoroutineScheduler.registerEvent$kotlinx_coroutines_test(TestCoroutineScheduler.kt:68)
at kotlinx.coroutines.test.StandardTestDispatcherImpl.dispatch(TestCoroutineDispatchers.kt:150)
at kotlinx.coroutines.internal.DispatchedContinuationKt.safeDispatch(DispatchedContinuation.kt:254)
at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:318)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:170)
at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
at io.ktor.server.testing.TestApplicationKt.runTestApplication(TestApplication.kt:534)
at io.ktor.server.testing.TestApplicationKt$testApplication$1.invokeSuspend(TestApplication.kt:517)
at _COROUTINE._BOUNDARY._(CoroutineDebugging.kt:42)
at io.ktor.test.dispatcher.TestCommonKt$runTestWithRealTime$1.invokeSuspend(TestCommon.kt:40)
at kotlinx.coroutines.test.TestBuildersKt__TestBuildersKt$runTest$2$1$1.invokeSuspend(TestBuilders.kt:317)
Caused by:
java.lang.IllegalStateException: Detected use of different schedulers. If you need to use several test coroutine dispatchers, create one `TestCoroutineScheduler` and pass it to each of them.
at kotlinx.coroutines.test.TestCoroutineSchedulerKt.checkSchedulerInContext(TestCoroutineScheduler.kt:257)
at kotlinx.coroutines.test.TestCoroutineScheduler.registerEvent$kotlinx_coroutines_test(TestCoroutineScheduler.kt:68)
at kotlinx.coroutines.test.StandardTestDispatcherImpl.dispatch(TestCoroutineDispatchers.kt:150)
at kotlinx.coroutines.internal.DispatchedContinuationKt.safeDispatch(DispatchedContinuation.kt:254)
at kotlinx.coroutines.internal.DispatchedContinuationKt.resumeCancellableWith(DispatchedContinuation.kt:318)
at kotlinx.coroutines.intrinsics.CancellableKt.startCoroutineCancellable(Cancellable.kt:26)
at kotlinx.coroutines.BuildersKt__Builders_commonKt.withContext(Builders.common.kt:170)
at kotlinx.coroutines.BuildersKt.withContext(Unknown Source)
at io.ktor.server.testing.TestApplicationKt.runTestApplication(TestApplication.kt:534)
at io.ktor.server.testing.TestApplicationKt$testApplication$1.invokeSuspend(TestApplication.kt:517)
....
Such pattern does work with runTest(standardTestDispatcher)
Aleksei Tirman [JB]
08/22/2025, 8:16 AMDispatchers.setMain
to override the dispatcher for the tests. You can find a more detailed explanation here.ursus
08/22/2025, 10:27 AMAleksei Tirman [JB]
08/22/2025, 10:44 AMtestApplication
to the runTest
.ursus
08/22/2025, 11:42 AMAleksei Tirman [JB]
08/22/2025, 12:40 PMursus
08/22/2025, 12:42 PMcombine
flow operator, and it sometimes seems to emit in different order, and then turbine assertion fails
which leads me to believe its not getting picked up
unless I dont know how StandardTestDispatcher
works..I thought it's singlethreadeness is what makes it deterministicAleksei Tirman [JB]
08/22/2025, 12:45 PMprivate val scheduler = TestCoroutineScheduler()
private val standardTestDispatcher = StandardTestDispatcher(scheduler)
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun test() {
println(standardTestDispatcher)
Dispatchers.setMain(standardTestDispatcher)
try {
testApplication {
println(currentCoroutineContext())
}
} finally {
Dispatchers.resetMain()
}
}
ursus
08/22/2025, 12:48 PMCURRENT=[
RunningInRunTest,
kotlinx.coroutines.test.TestCoroutineScheduler@2b8427f8,
kotlinx.coroutines.test.TestScopeKt$TestScope$$inlined$CoroutineExceptionHandler$1@3bd86d89,
ScopeCoroutine{Active}@bff2862,
Dispatchers.Default.limitedParallelism(1)
]
DESIRED=StandardTestDispatcher[scheduler=kotlinx.coroutines.test.TestCoroutineScheduler@2b8427f8]
are you expecting the dispatcher
to be there (or scheduler)?ursus
08/22/2025, 12:51 PM@KtorDsl
public fun testApplication(
parentCoroutineContext: CoroutineContext = EmptyCoroutineContext,
block: suspend ApplicationTestBuilder.() -> Unit
): TestResult = runTestWithRealTime {
runTestApplication(parentCoroutineContext, block)
}
public fun runTestWithRealTime(
context: CoroutineContext = EmptyCoroutineContext,
timeout: Duration = 60.seconds,
testBody: suspend CoroutineScope.() -> Unit
): TestResult = runTest(context, timeout) {
withContext(Dispatchers.Default.limitedParallelism(1), testBody)
}
...so it's not using the standard dispatcher, since it's referencing Disp.Default
which is not getting replaced?Aleksei Tirman [JB]
08/22/2025, 12:53 PMStandardTestDispatcher[scheduler=kotlinx.coroutines.test.TestCoroutineScheduler@60099951]
[RunningInRunTest, kotlinx.coroutines.test.TestCoroutineScheduler@60099951, kotlinx.coroutines.test.TestScopeKt$TestScope$$inlined$CoroutineExceptionHandler$1@3f11b1e0, ScopeCoroutine{Active}@3eff175e, Dispatchers.Default.limitedParallelism(1)]
ursus
08/22/2025, 12:54 PMursus
08/22/2025, 12:55 PMscheduler
, and not dispatcher
as wellAleksei Tirman [JB]
08/22/2025, 2:00 PM