Is this the intended use with StateFlow?
See original GitHub issueRecently I used turbine to test some StateFlow’s and came across a particular interaction and I’m mostly wondering what is the intended use. Both of the below cases fail.
oven.preWarm(300)
oven.states.test { state ->
assertEquals(WARMING(100), expectItem())
assertEquals(WARMING(200), expectItem())
assertEquals(PREWARM_COMPLETE, expectItem())
cancelAndIgnoreRemainingEvents()
}
For this case calling expectItem()
only receives the final state expected (PREWARM_COMPLETE) from the preWarm()
call and will fail the assertions. This makes sense as the collect operator is starting when our StateFlow has already emitted up to this state.
oven.states.test { state ->
assertEquals(WARMING(100), expectItem())
assertEquals(WARMING(200), expectItem())
assertEquals(PREWARM_COMPLETE, expectItem())
cancelAndIgnoreRemainingEvents()
}
oven.preWarm(300)
For this case there is an exception for a default timeout
:
kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
or for timeout = Duration.ZERO
java.lang.IllegalStateException: This job has not completed yet
However, when used like the below, moving the call to trigger state emission to the validate
lambda, all expected events are received and the test passes. This is because validate
is called after collection starts on the receiver flow.
oven.states.test { state ->
oven.preWarm(300)
assertEquals(WARMING(100), expectItem())
assertEquals(WARMING(200), expectItem())
assertEquals(PREWARM_COMPLETE, expectItem())
cancelAndIgnoreRemainingEvents()
}
And also launching in a new job and calling test and the emission after succeeds as well.
launch {
oven.states.test { state ->
assertEquals(WARMING(100), expectItem())
assertEquals(WARMING(200), expectItem())
assertEquals(PREWARM_COMPLETE, expectItem())
cancelAndIgnoreRemainingEvents()
}
oven.preWarm(300)
}
The documentation provided so far doesn’t provide much information for a best practice on how to handle Flows that have their emissions triggered elsewhere. What is the best practice here? I would assume that launching a new job should not be required.
Note: I am using this rule to runBlockingTests
@ExperimentalCoroutinesApi
class MainCoroutineRule(
val testDispatcher: TestCoroutineDispatcher = TestCoroutineDispatcher(),
) : TestWatcher() {
override fun starting(description: Description?) {
super.starting(description)
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description?) {
super.finished(description)
Dispatchers.resetMain()
testDispatcher.cleanupTestCoroutines()
}
/**
* A convenience function to prevent the need to call a long chain for the provided dispatcher
*/
fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) {
testDispatcher.runBlockingTest { block() }
}
}
Issue Analytics
- State:
- Created 3 years ago
- Reactions:5
- Comments:15 (1 by maintainers)
The version which calls
preWarm
inside of thetest
block is what I would recommend here and in general. The lifetime of the lambda which is passed totest
is the lifetime of the collection of the upstream flow. Any side-effects that you need to trigger should be done inside the lambda so that they can be observed.I can try to find a way to talk about this more clearly in the README or somewhere because I can see it not being entirely obvious how you write tests with side-effects that are needed to trigger events.
You are right @matthewcmckenna . As you suggested I moved
mainViewModel.setEvent(MainContract.Event.OnFetchPosts)
inside the.test
block and now I can retrieve all emitted events.I also wrote tests for
usecases
in my domain layer with Flow. It is super easy to test with Turbine 😃 now I want to test myViewModels
anduiStates
withStateFlows
. Thanks to you it works now