Hello, I'm trying to test a job, that runs an act...
# coroutines
p
Hello, I'm trying to test a job, that runs an action every 60 seconds. I'm using
TestCoroutineScope
for my test but it loops forever. Simplified test case:
Copy code
@Test
    fun a() = runBlockingTest {
        launch {
            while(true) {
                delay(60_000)
                println("called")
            }
        }

        advanceTimeBy(70_000)
    }
What am I missing?
w
Doesn’t
TestCoroutineScope
ignore all delays by default? I believe you need to call
pauseDispatcher
if you want to manually advance clock (and maybe trigger actions with
runCurrent()
)
t
The fact is, even if the
runBlockingTest
block has completed, if there are still running coroutines then the test won't cancel them and may suspend forever. You probably need to cancel the Job returned by
launch
yourself, after performing assertions.
p
Thank you for your insights. I looked into https://github.com/Kotlin/kotlinx.coroutines/blob/d7de5f5ba66a8d005e5cbd03b18522112303fd54/kotlinx-coroutines-test/src/TestCoroutineDispatcher.kt and I see that there is a queue and until it is not empty (it will never be is it is an infinite loop?) the
runBlockingTest
will not end. It looks weird to me that
advanceUntilTime
kinda does not do what the name suggest it won't "stop" the time. It would be nice that I could call
stop()
or something on the dispatcher ant it would "stop" the virtual time.
w
I could call stop() or something on the dispatcher ant it would “stop” the virtual time.
How does
stop()
that you suggest differ from the existing
pauseDispatcher()
method?
p
pauseDispatcher
does nothing in the given example it will still run indefinitely:
Copy code
@Test
    fun a() = runBlockingTest {
        pauseDispatcher()
        launch {
            while (true) {
                delay(60_000)
                println("called")
            }
        }

        advanceTimeBy(70_000)
    }
What I meant by
stop
is this:
Copy code
@Test
    fun a() = runBlockingTest {
        launch {
            while (true) {
                delay(60_000)
                println("called")
            }
        }

        advanceTimeBy(70_000)
        stop() // clear the queue
        // prints called only once
    }
I think
advanceByTime
should work like this by default. If one specified advance time by 70_000 it should stop after that time unless there are other caveats that I'm missing.
t
Tests executed with
runBlockingTest
usually fail fast if some coroutines are still running (and suspended) after test block ends. It seems that in this case, advancing time actually makes the launched coroutine react to it, conflicting with this feature. For the time being, you should be able to cancel running coroutines with the following code (which is equivalent to what you'd expect from a
stop
function):
Copy code
coroutineContext.cancelChildren()
p
l
@Paulius Ruminas Well, you basically have a infinite loop in a coroutine that is never cancelled. Keep a reference to the
Job
returned by
launch
, call
cancel()
on it and problem gone, logic correct.
p
@louiscad I'm talking about
TestDispatcher
implementation. Currently there is no way to stop the dispatcher from running automatically.
l
@Paulius Ruminas That changes nothing. I'm not talking about stopping the dispatcher, which is not even something I'm doing in tests or common cases, I'm talking about stopping (cancelling) your coroutine that is a infinite loop.
while(true) {
p
We are talking about different thing here 🙂. Yes I could do it like this:
Copy code
suspend fun runIndefinitely() {
        while (true) {
            delay(60_000)
            println("I'm called every minute")
        }
    }

    @Test
    fun test1() = runBlockingTest {
        val explicitJobOnlyForTest = Job()

        try {
            launch(coroutineContext + explicitJobOnlyForTest) {
                runIndefinitely()
            }

            advanceTimeBy(120_000)
        } finally {
            explicitJobOnlyForTest.cancelAndJoin()
        }
    }
But I would like that TestDispatcher would let me avoid all of this explicit cancellation like this:
Copy code
@Test
    fun test2() = runBlockingTest {
        launch {
            runIndefinitely()
        }

        advanceTimeBy(120_000)
        // currently does not work since runBlockingTest calls advanceUntilIdle
    }
l
I don't see why it should add "magic" behavior like that. To me, a dispatcher should never decide to cancel coroutines. Only the
Job
from the scope/coroutineContext should.