https://kotlinlang.org logo
Title
p

Paulius Ruminas

04/09/2020, 9:19 AM
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:
@Test
    fun a() = runBlockingTest {
        launch {
            while(true) {
                delay(60_000)
                println("called")
            }
        }

        advanceTimeBy(70_000)
    }
What am I missing?
w

wasyl

04/09/2020, 9:29 AM
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

tseisel

04/09/2020, 9:37 AM
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

Paulius Ruminas

04/09/2020, 10:11 AM
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

wasyl

04/09/2020, 10:27 AM
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

Paulius Ruminas

04/09/2020, 11:10 AM
pauseDispatcher
does nothing in the given example it will still run indefinitely:
@Test
    fun a() = runBlockingTest {
        pauseDispatcher()
        launch {
            while (true) {
                delay(60_000)
                println("called")
            }
        }

        advanceTimeBy(70_000)
    }
What I meant by
stop
is this:
@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

tseisel

04/09/2020, 11:49 AM
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):
coroutineContext.cancelChildren()
p

Paulius Ruminas

04/09/2020, 11:54 AM
l

louiscad

04/09/2020, 12:01 PM
@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

Paulius Ruminas

04/09/2020, 12:19 PM
@louiscad I'm talking about
TestDispatcher
implementation. Currently there is no way to stop the dispatcher from running automatically.
l

louiscad

04/09/2020, 12:21 PM
@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

Paulius Ruminas

04/09/2020, 12:46 PM
We are talking about different thing here 🙂. Yes I could do it like this:
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:
@Test
    fun test2() = runBlockingTest {
        launch {
            runIndefinitely()
        }

        advanceTimeBy(120_000)
        // currently does not work since runBlockingTest calls advanceUntilIdle
    }
l

louiscad

04/09/2020, 2:01 PM
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.