If I turn regular Flows into StateFlows `stateFlow...
# flow
a
If I turn regular Flows into StateFlows
stateFlow1
and
stateFlow2
via
.stateIn(started = SharingStarted.WhileSubscribed)
and then create
stateFlowCombined = stateFlow1.combine(stateFlow2).startIn(started = SharingStarted.WhileSubscribed)
then will subscribing to
stateFlowCombined
transitively subscribe to
stateFlow1
and
stateFlow2
? I'm struggling with writing unit tests to verify behavior that works fine in our production app.
n
Yes subscribing to
stateFlowCombined
will subscribe to
stateFlow1
and
stateFlow2
. but it may not be immediate since
stateIn
and
combine
launch coroutines to subscribe upstream. Test dispatchers don't auto-run nested launched coroutines, iirc. Did you try using
runCurrent()
to force any pending coroutines to actually run before verifying behavior?
a
I used
advanceUntilIdle()
though I confess I'm not certain I understand the difference.
n
That should have worked too.
runCurrent
will run everything that's ready to run without advancing the fake test clock.
advanceUntilIdle
will run coroutines that are waiting for a
delay
too and it will advance the fake test clock as need to execute them.
a
Got it, thanks. The problem may have been caused by my tests not properly subscribing to
stateFlowCombined
in the first place; I ended up using a solution like https://kotlinlang.slack.com/archives/C5HT9AL7Q/p1655856058094669 and it seems to be working for me.
n
still wise to use
runCurrent
after those jobs launch before
block.invoke()
, imo. If those where to launch anything internally, I suspect you'd run into the same issue with code not running before you verify.
It's usually discouraged to pass a scope into a suspend method, usually you want to just start coroutines (no suspend) or return only after all coroutines have finished in which case you should use
coroutineScope
block.
Though I'll sometimes pass in TestScope/TestCoroutineScope for test helpers to gain access to
runCurrent
method.
a
So something more like
Copy code
fun observeStateFlows(
    testScope: TestScope,
    vararg stateFlows: StateFlow<Any?>,
    block: () -> Unit
) {
    val observationJobs = stateFlows.map { flow ->
        testScope.launch {
            flow.collect {}
        }
    }

    try {
        testScope.runCurrent()
        block.invoke()
    } finally {
        observationJobs.forEach { it.cancel() }
    }
}
If I want to observe the flows while my test code is running, I'll need to launch the nop collectors on some scope, right?
(I suppose I could also make it an extension function on TestScope to simplify things a bit)
n
cancel
is async. It sets up the cancellation process but doesn't wait for cancellation to finish. So your observeStateFlows method can finish while the passed in flows are still running. If you need a CoroutineScope inside a suspend method, use the
coroutineScope
method. You can use
TestScope.testScheduler
to access the test clock manipulation without the danger of launching coroutines that might last beyond the method call.
Copy code
suspend fun observeStateFlows(
    scheduler: TestScheduler,
    vararg stateFlows: StateFlow<Any?>,
    block: suspend () -> Unit
) = coroutineScope { // <- waits for observationJobs to all finish canceling
    val observationJobs = stateFlows.map { flow ->
        flow.launchIn(this) //same as launch/collect
    }

    try {
        scheduler.runCurrent()
        block.invoke()
    } finally {
        observationJobs.forEach { it.cancel() } //does not wait for the jobs to actually cancel
    }
} //coroutineScope actually waits for its child jobs to finish cancel.
And technically the try/finally is not quite needed since if block throws, it'll fail the parent which cancels all children
a
Ah, that makes sense. Thanks!