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