Can someone help me understand about `delay()` s i...
# coroutines
t
Can someone help me understand about
delay()
s inside a
runTest{}
? In my mind the delays would wait until I call
advanceTimeBy()
but in practice the test just run and ignore all the delays. What’s the point of the
advanceTimeBy()
then?
u
any code?
t
Not necessary, my question was more in the theory part of it. But we can use the code in the readme as example. Take a look here: https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-test/README.md#controlling-the-virtual-time In this code it’s “controlling” the time, but, if I remove everything after the
launch
block, it still skip the delays and print the numbers instantly. That’s why I’m confused with the use of
advanceTimeBy()
. I see that when you advance the time it changes the
currentTime
variable, but what uses this? I would imagine delays would depend on it but doesn’t seems like the case.
n
Your understanding is basically correct. The piece you are missing is that
runTest
does some extra stuff after it runs your lambda. It calls
advanceUntilIdle
and checks that all your coroutines actually finish.
delay
does wait, but then it is run during the cleanup phase of
runTest
.
Consider:
Copy code
class UnderTest(val scope: CoroutineScope) {
        var x: Int = 0
        fun doSomething() {
            scope.launch {
                delay(100)
                x = 1
                delay(100)
                x = 2
            }
        }
    }

    @Test
    fun testFoo() = runTest {
        val underTest = UnderTest(this)
        underTest.doSomething()
        advanceTimeBy(150)
        assertEquals(1, underTest.x)
    }
u
Hmm, so injecting scope idiomatic? As opposed to creating it privately, if UnderTest has a natural lifecycle (destructor)
n
I recommend it if only for injecting test scopes.
It can also work nicely in situations where an entire tree of loosely coupled instances share a lifecycle, they can all share the scope and just register
invokeOnCompletion
listeners.
u
Hm, I always felt dangerous to inject the scope, since then you dont control it, anybody can cancel you
n
idk that i'd do it for a library API or anything that might be called from Java.
lol, exactly. If you are writing at the app level though, then "anybody" is more limited.
u
If I create the scope privately, could runTest work at all?
n
Even inside a library, internal helpers could take the scope controlled privately by the publicly exposed classes.
Not really, you need some way to inject it. It could be an internal constructor though just for testing.
u
Copy code
class Foo(dispatcher: Dispatcher) : Scoped {
	private val scope = SupervisorScope(dispatcher)

	override fun clear() {
		scope.cancel()
	}
}

class Foo(private val scope: CoroutineScope)
I'm mostly talking about this. I could see your point but, Scoped interface predates coroutines in my code, and also has a rxjava implementation (CompositeDisposable) But more over what I fear is that since now scope is injected, its DI responsibility, and there fore I need a singleton CoroutineScope per semantic DI scope, and since scopes are nested I'd need a qualifier
and I fear one could mess up by having the Foo be @UserScope and injecting @AppScope coroutine scope etc..a possibly footgun
btw if I were to clear some files on scope teardown, is there some way to do it when the scope is injected? I faintly remember something like
invokeOnCancelation
or something
n
It's invokeOnCompletion
u
Thanks. So you dont worry about mismatching the scopes?
n
I actually got tired of DI related bugs and ditched it for awhile 😅 so I haven't run into that issue. Sounds like a question worth a new thread.
I suspect an alternative would be to remove the nesting and then manually link the components.
u
Nah, nesting is good, slack: AppScope > WorkspaceScope > ChannelScope etc. Very helpful
t
@Nick Allen thanks for your reply! So the “secret” is passing the scope so the delay is controlled? If I run the same test you wrote without passing the scope then the delays would not be controller by
advanceTimeBy()
correct?
n
Right. The test and tested code need to be using the same scope. Sharing the dispatcher allows the test to control the timing of the tested code. Sharing the
Job
allows
runTest
to verify that you aren't leaking coroutines using that scope.
u
what if I only passin the dispatcher via ctor, as was the norm so far?
n
Then the CoroutineExceptionHandler that
runTest
installs will not do it job and report uncaught exceptions (JVM on desktop just swallows them iirc)
runBlockingTest
had coroutine leak checking behavior that would be missed if you didn't pass in the
Job
, I'm not seeing that in the
runTest
source so maybe the ditched it (though can't be sure, not like I scoured the code).
u
thats sucks, I dont feel comfortable injecting coroutine scope into my stuff, it might get mismatched with object's DI scope (hence why I created coroutine scope in such objects privately)
n
Copy code
@JvmInline value class AppScope(private val scope: CoroutineScope) : CoroutineScope by scope
@JvmInline value class UserScope(private val scope: CoroutineScope) : CoroutineScope by scope
Different types is the easiest workaround I can think of to make mix ups less likely.
u
well, you can still mismatch the dagger scope annotation with this, i.e. having the Foo be @UserScope and yet injecting yours App(Coroutine)Scope