Do you have tips for testing code with `delay` ? T...
# kotest
m
Do you have tips for testing code with
delay
? There's a function that's supposed to run periodically and I want to check that it's running the expected number of times in a given interval. From the kotest documentation I gathered that I have to set
coroutineTestScope = true
. The additional explanation in the kotlinx-coroutines-test repository suggests that it should be possible to control execution using
advanceTimeBy
. But when running my test, all invocations of
delay
are just skipped without anyone having called
advancedTimeBy
, essentially resulting in an infinite loop that just tries to run the code as quickly as possible. I have tried adding both
testCoroutineScheduler.runCurrent()
and
testCoroutineScheduler.advanceUntilIdle()
to indicate that it's supposed to stop afterwards but that didn't make any difference. I suppose that maybe my instance under test runs inside the wrong coroutine context or scope but I don't know which
coroutineContext
to pass in.
o
Delay skipping is intentional with the test scheduler. Basically, concurrent
delay
invocations just define the order in which the invocations complete. Everything then runs as fast as possible (that's what virtual time is for). You should almost never need to invoke test scheduler functions manually. Of course, not using
coroutineTestScope
will get you real-time behavior. But expecting that in tests is not just slow. It also makes them unstable (machine load varies). So using the test scheduler, you could do the following (this is kotlin-test, but you get the idea):
Copy code
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import kotlin.test.Test
import kotlin.time.Duration.Companion.seconds

class XTest {
    @Test
    fun test1() = runTest {
        var job1IterationCount = 0
        var job2IterationCount = 0

        coroutineScope {
            launch {
                for (i in 1..50) {
                    delay(10.seconds)
                    print("1")
                    job1IterationCount++
                }
            }
            launch {
                for (i in 1..5) {
                    delay(30.seconds)
                    print("2")
                    job2IterationCount++
                }
            }
        }

        println("\njob1IterationCount=$job1IterationCount, job2IterationCount=$job2IterationCount")
    }
}
Because the test dispatcher always operates with a single thread, you don't have to worry about thread-safe mutations. Does that help?
m
Unfortunately I think that doesn't help much in this given scenario. Ideally I could advance time by controlled intervals and then check the events / flow items that were produced during that time. In production, this task runs forever (while certain conditions are met) which means that if
delay
returns immediately, this essentially becomes an infinite loop. What's confusing is that the
README
of
kotlinx-coroutines-test
suggests that
delay
can be suspended until
advanceTimeBy
is called. If I could get it to work like that, everything would be great.
Also tried wrapping it with
testScheduler.timeSource.measureTime
as I thought that maybe one needs to explicitly state to use the special
TimeSource
. Unfortunately that didn't work either. I think I'll have to postpone this specific test for now. Its ROI is becoming quite bleak otherwise 😬 😅
o
check the events / flow items that were produced during that time
Can you make the event cadence also depend on virtual time?
m
I can control the coroutine context used to
launch
its Job. But I can't seem to get
delay
to actually wait until the test invokes
advanceTimeBy
. Not sure what else might be needed to have it wait on the virtual time being advanced.
o
Do you have a simple code example to illustrate what you are trying to achieve?
m
boiled down to essentials:
Copy code
class ClassUnderTest(override val coroutineContext: CoroutineContext) : CoroutineScope {

    data class Status(val foo: String = "bar")
    
    private val flow =
        MutableSharedFlow<Status>(replay = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST)

    fun incoming(): Flow<Status> = flow

    fun checkCurrentStatus(): Status = Status()

    fun monitoringJob(): Job = launch {
        while (isActive) {
            delay(15.minutes) // actually a configurable interval
            flow.emit(checkCurrentStatus())
        }
    }
}
Now I want to test that when the Job from
monitoringJob
is started, a new element is emitted into
flow
in the specified intervals. So after advancing time by 15 minutes, there should be 1 element in the flow, after 30 minutes it should be 2.
o
OK, if that is launched in a test dispatcher scope, and your receiver uses
delay
for its intervals as well, why shouldn't it just work (without
advanceTimeBy
and other scheduler methods)?
m
I actually hadn't tried using
delay
in the receiver as well 🤔 up until now, the receiver just collected everything from the flow as fast as possible and I tried using
advanceTimeBy
to check what it had collected so far. I'll look into changing it - thanks for the idea!
👍 1
o
"as fast as possible" would be depending on real time. It's always important never to mix real and virtual time.