ursus
08/21/2025, 7:24 PMclass FooTest {
private val myDependency = Dependency(StandardTestDispatcher(testScheduler)) <----
@Test fun foo() = runTest {
// val dispatcher = StandardTestDispatcher(testScheduler)
...
}
}
I like to construct dependencies to the sut in properties, but testScheduler is only available from inside the runTest lambda, which is "too late" for me.
How do I access testScheduler from a property?Oliver.O
08/21/2025, 8:22 PMtestScheduler at the test class level.
For more flexibility, you could consider a modern coroutines-first test framework like TestBalloon (I'm the author). For your use case, suspending test fixtures seem to be well suited.ursus
08/21/2025, 8:24 PMOliver.O
08/21/2025, 8:29 PMrunTest relies on everything requiring coroutines to happen inside runTest. Don't pass anything in, don't let anything out.ursus
08/21/2025, 8:30 PMrunTest ?Oliver.O
08/21/2025, 8:32 PMursus
08/21/2025, 8:34 PMrunTest?Oliver.O
08/21/2025, 8:42 PMrunTest inside for compatibility with kotlinx-coroutines-test, but it's way more than that:
TestBalloon provides a coroutine hierarchy across test suites and tests, (suites can nest). So you can structure your tests according to your needs and share what you call a dependency between any number of suites and tests.
Also, runTest can be disabled when you test stuff on a "real" dispatcher without virtual time.
Getting all of this right (and multiplatform) is extremely easy to use, but not trivial to implement. You might want to look at @CLOVIS' article for a good overview about the evolution of Kotlin testing.ursus
08/21/2025, 8:44 PMOliver.O
08/21/2025, 8:46 PMursus
08/21/2025, 8:47 PMOliver.O
08/21/2025, 8:50 PMursus
08/21/2025, 8:50 PMursus
08/21/2025, 8:51 PMclass FooTest {
private val testScheduler = TestCoroutineScheduler()
private val testDispatcher = StandardTestDispatcher(testScheduler)
private val myDependency = Dependency(testDispatcher) <----
@Test fun foo() = runTest(testDispatcher) {
...
}
}
and it does seem to workOliver.O
08/21/2025, 8:57 PMursus
08/21/2025, 8:58 PMOliver.O
08/21/2025, 8:59 PMursus
08/21/2025, 8:59 PMOliver.O
08/21/2025, 9:00 PMursus
08/21/2025, 9:00 PMursus
08/21/2025, 9:01 PMOliver.O
08/21/2025, 9:02 PM@Before and @After functions for necessary cleanup. Some coroutine context elements mandate such cleanup like dispatchers creating their own threads.ursus
08/21/2025, 9:03 PMOliver.O
08/21/2025, 9:06 PMrunTest?ursus
08/21/2025, 9:08 PMOliver.O
08/21/2025, 9:09 PMGlobalScope with all of its caveats regarding runaway coroutines. Normally, runTest would take care of that.ursus
08/21/2025, 9:10 PMclass MyDependency(private val dispatcher) {
private val scope = CoroutineScope(disptacher + SupervisorJob())
// ...
fun cancel() {
scope.cancel()
}
}
if I have something like this, and I pass in the StandardTestDispatcher derived from runTest , the scope would get autocancelled?Oliver.O
08/21/2025, 9:13 PMrunTest provides such a scope for "normal" coroutines as well as a background scope for coroutines that will be auto-cancelled eventually.ursus
08/21/2025, 9:14 PMclass MyDependency(private val scope: CoroutineScope) and then runTest { .. MyDependency(this) }?Oliver.O
08/21/2025, 9:15 PMursus
08/21/2025, 9:16 PMursus
08/21/2025, 9:18 PMOliver.O
08/21/2025, 9:22 PMursus
08/21/2025, 9:23 PMOliver.O
08/21/2025, 9:23 PMursus
08/21/2025, 9:24 PMursus
08/21/2025, 9:24 PMursus
08/21/2025, 9:25 PMOliver.O
08/21/2025, 9:29 PMursus
08/21/2025, 9:30 PMCoroutineScope instanceOliver.O
08/21/2025, 9:31 PMGlobalScope implicitly, you're not. There is no parent handling your exceptions. Like it was in thread land.ursus
08/21/2025, 9:32 PMOliver.O
08/21/2025, 9:33 PMrunTest { .. MyDependency(this) }.ursus
08/21/2025, 9:34 PM@SingleIn(UserScope::class)
class MyDependency(
@QualifierFor(AppScope::class) private val scope: CoroutineScope
) {
fun foo() {
scope.launch {
// access other fields
}
}
}
that this compiles & is a memory leakursus
08/21/2025, 9:35 PMursus
08/21/2025, 9:37 PMMyDependency this .. okay it is a leakOliver.O
08/21/2025, 9:38 PMursus
08/21/2025, 9:38 PMursus
08/21/2025, 9:39 PMOliver.O
08/22/2025, 2:00 PMrunTest, you could run into trouble if that dependency uses CoroutineStart.LAZY. In that case, it would never run if no one is waiting for it.
What would be the main motivation for using properties and magic re-instantiation at all instead of putting everything into runtest, using local variables, and make re-use explicit by calling a utility function?