Aidan Low
06/21/2022, 5:29 PMstateFlow1
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.Nick Allen
06/21/2022, 10:03 PMstateFlowCombined
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?Aidan Low
06/22/2022, 12:07 AMadvanceUntilIdle()
though I confess I'm not certain I understand the difference.Nick Allen
06/22/2022, 12:14 AMrunCurrent
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.Aidan Low
06/22/2022, 12:16 AMstateFlowCombined
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.Nick Allen
06/22/2022, 12:47 AMrunCurrent
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.Nick Allen
06/22/2022, 12:50 AMcoroutineScope
block.Nick Allen
06/22/2022, 12:50 AMrunCurrent
method.Aidan Low
06/22/2022, 12:56 AMfun 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?Aidan Low
06/22/2022, 12:58 AMNick Allen
06/22/2022, 4:02 AMcancel
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.
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 childrenAidan Low
06/22/2022, 1:45 PM