kevin.cianfarini
11/10/2023, 6:47 PMStateFlow
instances aren’t guaranteed to emit each state change to subscribers in the kotlinx docs? I’m thinking of the following scenario:
val sf = MutableStateFlow<Int>(0)
sf.value = 1
sf.value = 0
Any subscriber on sf
would not receive any emission because the value of the stateflow quickly flips from 0 to 1, and then 1 to 0 before dispatch can occur. Since stateflows are always conflated, and the initial value was 0, a new emission would not occur.
For practical applications this is fine, but under test this crops up regularly. A real world example is in the thread. I’ve run into this problem many times and it still catches me by surprise occasionally.kevin.cianfarini
11/10/2023, 6:47 PM@Test fun `sends analytic screen events`() = runTest {
val lifecycle = TestLifeCycle()
val analyticsProvider = FakeAnalyticsProvider()
val (flow, _) = sut(lifeCycle = lifecycle, analytics = analyticsProvider)
backgroundScope.launch { flow.collect() }
analyticsProvider.trackedScreens.test {
assertEquals(expected = AnalyticsScreen.TariffDetails, actual = awaitItem().first)
lifecycle.currentState.update { PlatformLifecycle.State.Stopped }
yield()
expectNoEvents()
lifecycle.currentState.update { PlatformLifecycle.State.Started }
assertEquals(expected = AnalyticsScreen.TariffDetails, actual = awaitItem().first)
}
}
kevin.cianfarini
11/10/2023, 6:49 PMyield
call in the above test is necessary to ensure that the state change has a chance to emitkevin.cianfarini
11/10/2023, 7:05 PMkevin.cianfarini
11/10/2023, 7:37 PMPablichjenkov
11/10/2023, 7:44 PMPablichjenkov
11/10/2023, 7:46 PMkevin.cianfarini
11/10/2023, 7:51 PMPablichjenkov
11/10/2023, 8:02 PMkevin.cianfarini
11/10/2023, 8:03 PMkevin.cianfarini
11/10/2023, 8:03 PMPablichjenkov
11/10/2023, 8:04 PMPablichjenkov
11/10/2023, 8:06 PMkevin.cianfarini
11/10/2023, 8:08 PMkevin.cianfarini
11/10/2023, 8:08 PMBill Phillips
11/10/2023, 8:08 PMI don’t think there’s a good way to test all state changes of a state flow are emitted.stateflow is designed to ensure that not every emission makes it to all collectors. so your intuition is correct
Bill Phillips
11/10/2023, 8:09 PMPablichjenkov
11/10/2023, 8:12 PMPatrick Steiger
11/12/2023, 2:46 PMkevin.cianfarini
11/12/2023, 2:52 PMrkeazor
11/13/2023, 5:56 AMdarkmoon_uk
11/13/2023, 6:50 AMrunCurrent
between value changes to process emissions; probably mimicking the real-world conditions.Dmitry Khalanskiy [JB]
11/13/2023, 10:34 AMsf.value = 1
and sf.value = 0
happen in quick succession, it isn't at all different from sf.value = 0
. Therefore, it doesn't make sense to test what happens between sf.value = 1
and sf.value = 0
. You only need to test what happens between them if, in production, there can be a noticeable delay between them, like a network call or a database access. In this case, you can emulate the delay by literally inserting delay(25.milliseconds)
or something like that in the code that mocks these I/O operations. When you don't have a delay, your test answers literally this question: "What behavior will we observe if the network call happens instantaneously?" This may also be a valid question, depending on your requirements, by the way.
I see that several people in this thread think that the tests behaving exactly as production does is undesirable. Can someone explain to us why sticking a delay
between emissions is not always a good solution?kevin.cianfarini
11/13/2023, 11:34 AMDmitry Khalanskiy [JB]
11/13/2023, 11:57 AMUpdates to the value are always conflated.
conflated
links to https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/conflate.html
The docs for MutableStateFlow
https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-mutable-state-flow/ also mention this:
Note that all emission-related operators, such as value's setter, emit, and tryEmit, are conflated using Any.equals.Where else do you think we need to write about it?
kevin.cianfarini
11/13/2023, 12:10 PMUpdates to the value are always conflated. So a slow collector skips fast updates, but always collects the most recently emitted value.This can also be called from a really fast producer which doesn't necessarily have to be executing on a coroutine dispatcher. Therefore in this scenario every collector is considered slow because it's perhaps impossible to dispatch a flow collection quick enough to capture intermediate values. I suppose the same is true of something like
channelFlow { ... }.conflate()
So maybe this is a moot point.kevin.cianfarini
11/13/2023, 12:11 PMFlow.conflate
operator much, but I do use StateFlow rather frequently, so I forgot about that 😄Pablichjenkov
11/13/2023, 12:37 PMDmitry Khalanskiy [JB]
11/13/2023, 12:48 PMdelay
calls in the test dispatchers are completely deterministic, and add nothing to the test execution time.Pablichjenkov
11/13/2023, 1:14 PMkevin.cianfarini
11/13/2023, 1:15 PMDmitry Khalanskiy [JB]
11/13/2023, 1:17 PMPablichjenkov
11/13/2023, 1:36 PMDmitry Khalanskiy [JB]
11/13/2023, 1:58 PMStateFlow
from several threads in parallel? Or not even necessarily code, but just a clear verbal explanation of what that code would do and what properties of that code you'd like to test.Dmitry Khalanskiy [JB]
11/13/2023, 2:35 PMStateFlow
without missing any emissions: https://github.com/Kotlin/kotlinx.coroutines/issues/3939kevin.cianfarini
11/13/2023, 2:36 PMDmitry Khalanskiy [JB]
11/13/2023, 2:37 PMkevin.cianfarini
11/13/2023, 2:38 PMBill Phillips
11/13/2023, 5:32 PMPablichjenkov
11/13/2023, 6:20 PMkevin.cianfarini
11/13/2023, 6:23 PMPablichjenkov
11/13/2023, 6:28 PMkevin.cianfarini
11/13/2023, 7:30 PMPablichjenkov
11/13/2023, 7:37 PMgildor
11/14/2023, 3:43 AMdarkmoon_uk
11/14/2023, 3:45 AMrunCurrent
between the StateFlow
value changes handles it; this isn't a really complex problem just something you have to consider when writing tests.Pablichjenkov
11/14/2023, 5:21 AM