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?