https://kotlinlang.org logo
Title
n

Nino

10/14/2021, 11:01 AM
Hello, I don't understand why I can't
delay
the emission of values in a flow, during a unit test... The following code fails :
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.TestCoroutineDispatcher
import kotlinx.coroutines.test.TestCoroutineScope
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.setMain
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.rules.TestRule
import org.junit.runner.Description
import org.junit.runners.model.Statement

class CatRepository(
    private val coroutineDispatcher: CoroutineDispatcher
) {
    fun getMeowsFlow() = flow {
        emit("Meow #1")

        delay(3_000)

        emit("Meow #2")
    }.flowOn(coroutineDispatcher)
}

class CatRepositoryTest {
    @get:Rule
    val testCoroutineRule = TestCoroutineRule()

    private val catRepository = CatRepository(testCoroutineRule.testCoroutineDispatcher)

    @Test
    fun catShouldMeowOnce() = testCoroutineRule.runBlockingTest {
        pauseDispatcher()

        assertEquals(
            listOf("Meow #1"),
            catRepository.getMeowsFlow().toList()
        )
    }
}

class TestCoroutineRule : TestRule {
    val testCoroutineDispatcher = TestCoroutineDispatcher()
    private val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)

    override fun apply(base: Statement, description: Description?) = object : Statement() {
        @Throws(Throwable::class)
        override fun evaluate() {
            Dispatchers.setMain(testCoroutineDispatcher)

            base.evaluate()

            Dispatchers.resetMain() // reset main dispatcher to the original Main dispatcher
            testCoroutineScope.cleanupTestCoroutines()
        }
    }

    fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
        testCoroutineScope.runBlockingTest { block() }
}
It prints
expected:<[Meow #1]> but was:<[Meow #1, Meow #2]>
Expected :[Meow #1]
Actual   :[Meow #1, Meow #2]
It makes no sense at all ! Since the virtual time is at 0ms, how come the
delay
is completely ignored ? Could someone explain ?
j

Joffrey

10/14/2021, 11:04 AM
I'm not entirely sure how the virtual time works in
kotlinx-coroutines-test
, but in any case
toList()
would hang forever if the
flow
didn't progress
n

Nino

10/14/2021, 11:04 AM
Doesn't it collect only the available values at that time ?
j

Joffrey

10/14/2021, 11:05 AM
No, it's a terminal operator that goes to flow completion
There is no real notion of "values at this time" in a flow, because there could be no buffer at all. The collector is the one that asks for elements from the producer
n

Nino

10/14/2021, 11:06 AM
So time manipulation has no effect on flows ? 😞
@Test
    fun catShouldMeow2After3000ms() = testCoroutineRule.runBlockingTest {
        advanceTimeBy(3_001)

        assertEquals(
            "Meow #2",
            catRepository.getMeowsFlow().first()
        )
    }
This test fails too, that's so disappointing 😞
j

Joffrey

10/14/2021, 11:08 AM
But you're asking for the first element of the flow here, flows are cold, not hot (at least most of them). By default, they don't progress independently of their collector. So
first()
here should return the first element, no matter how long you wait before calling it.
e

ephemient

10/14/2021, 8:10 PM
exactly. the flow starts when the collector starts, advancing time before that happens has no impact
if you want to test what's happening during collection, try https://github.com/cashapp/turbine
m

myanmarking

10/15/2021, 9:40 AM
you are advancing the time before collecting. Try collecting inside a launch, and then advance the time
or just
launch{flow.toList} delay(x)