```class FooTest { private val myDependency = Dep...
# test
u
Copy code
class 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?
o
You don't. Traditional frameworks like kotlin-test instantiate your test class outside any coroutine. So there is no
testScheduler
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.
u
I see, but that would be a big migration.. maybe just create it myself and then pass it down to runTest?
o
You can use a modern framework alongside a traditional one.
runTest
relies on everything requiring coroutines to happen inside
runTest
. Don't pass anything in, don't let anything out.
u
do people just create the sut and all the dependencies inline in the test case with
runTest
?
o
Either that, or they use Kotest, which also provides coroutines.
u
so with your framework you're wrapping
runTest
?
o
Yes, if you want it, there's
runTest
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.
❤️ 1
u
Yea I'm just trying to figure out if this is dsl sugar for runTest or some new runner etc
o
Not sugar at all. It's an entirely different architecture, giving you access to test patterns that just did not exist in the old days while maintaining compatibility with the underlying ecosystem.
u
right, but who is running the tests then, junit still?
o
Mainly the framework itself, after using its own compiler plugin to discover top-level test suites. For ecosystem compatibility, JUnit Platform is used on the JVM to bootstrap the framework. On other targets it is different.
u
I see, I'll look into it, thanks!
👍 1
btw I've tried to
Copy code
class 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 work
o
In effect, you are creating global objects with infinite lifetime. In some cases it might just work, but it's not what coroutines were designed for.
u
what do you mean? properties are per test case
o
Who cleans up your test class after it was instantiated?
u
do you mean the testScheduler needs closing?
o
Wait, you're on the JVM where each test class gets re-instantiated per test case, right?
u
yea
the properites are basically just "dsl" for instantiating dependencies, visually separate them from the test; not that something is shared across functions
o
OK, then you might get away with
@Before
and
@After
functions for necessary cleanup. Some coroutine context elements mandate such cleanup like dispatchers creating their own threads.
u
but what cleanup - in this case? (the sample)
o
In your case, there seems to be no cleanup needed for the scheduler and dispatcher themselves. But what will your dependency do? Create coroutines outside
runTest
?
u
technically yea, create its own scope driven by the dispatcher passed in ..and yea that scope needs explicit closing sure
o
So there you're basically on
GlobalScope
with all of its caveats regarding runaway coroutines. Normally,
runTest
would take care of that.
u
how so?
Copy code
class 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?
o
Structured concurrency would require you to pass in a parent scope (with a parent job).
runTest
provides such a scope for "normal" coroutines as well as a background scope for coroutines that will be auto-cancelled eventually.
u
so my shape should look like
class MyDependency(private val scope: CoroutineScope)
and then
runTest { .. MyDependency(this) }
?
o
Exactly.
u
yea .. I was never a fan of that, since at runtime there's multiple semantic scopes in my project (think LoggedInScope, LoggedOutScope etc), and idk, it feels brittle .. since it possible to mismatch the coroutine scope with DI scope, causing leaks
BTW is that test time cleanup such a big deal? I mean tests run in a process, which then dies..whatever? It's not like that memory is gone forever
o
As always, it depends. For small projects, you probably, won't care. If you run 40k tests on CI, someone will be paying a price. The bigger problem is having tests fail in unpredictable ways due to runaway coroutines. They could do stuff concurrently, produce uncaught exceptions at times when other tests run, mutate state, ...
u
well, that assumes some shared state across tests in order to have some effect
o
Of course.
u
but I see your point - but to be frank, I'd rather have CI sweat then cause memory leaks in the app, and I don't really see a way of making that solid
only maybe via linting, but eh, that eats CI time as well
or do you see something better?
o
In particular regarding anything concurrent (which coroutines are by their nature), I'll always try not to code against the grain. Unless I know excactly what I'm doing any why. So I usually stick strictly with structured concurrency and scoped resource usage, and that's one area where Kotlin excels. We just have to drop legacy Java habits that make code brittle and slow (reflection).
u
am I not using structured concurrency or something? it's just a matter of who owns the
CoroutineScope
instance
o
In the property case, effectively using
GlobalScope
implicitly, you're not. There is no parent handling your exceptions. Like it was in thread land.
u
which case, the test or the `class MyDependency..`sample?
o
Both. The only safe variant was
runTest { .. MyDependency(this) }
.
u
for tests sure, I'm talking runtime now, since the issue with your approach is
Copy code
@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 leak
now that I've typed it out, I'm not sure it is a leak..
so scope holds onto that lamba, which captures
MyDependency
this
.. okay it is a leak
o
Now we're in DI land. I guess there's other people who are better qualified to comment on that.
u
well afaik there is no good solution other than don' mess it up/linting aligning it
hence why I opted for creating my scope privately, as to never mismatch them
o
One more thing, back to testing: If your dependency creates its own coroutines without a parent in
runTest
, 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?