https://kotlinlang.org logo
#coroutines
Title
# coroutines
k

kevin.cianfarini

11/10/2023, 6:47 PM
Is there any interest in calling out that
StateFlow
instances aren’t guaranteed to emit each state change to subscribers in the kotlinx docs? I’m thinking of the following scenario:
Copy code
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.
💯 1
Copy code
@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)
        }
    }
The
yield
call in the above test is necessary to ensure that the state change has a chance to emit
@Bill Phillips Might be interested to chime in here, too
p

Pablichjenkov

11/10/2023, 7:44 PM
I think it should. It mentions somewhere StateFlow is conflated but that word is not too popular especially non native English speakers
👍 1
Even better provide a guide on how to test all the emissions went through. Like your code snippet .
k

kevin.cianfarini

11/10/2023, 7:51 PM
I ended up rewriting my test to avoid the yield, as it was flaky. I don’t think there’s a good way to test all state changes of a state flow are emitted. That requires careful coordination of the thing producing new state values and what’s consuming them on the receiving end. Interleaving producers and receivers will ensure events all events get delivered, but that’s not always possible — especially if the stateflow is an implementation detail of something under test which you don’t have easy control over.
p

Pablichjenkov

11/10/2023, 8:02 PM
I mean you could test it by encapsulating it in an event recording class. But it will add overhead to the code, definitely. You would have to expose the private StateFlow for testing I believe
k

kevin.cianfarini

11/10/2023, 8:03 PM
That requires you alter your code under test though
I’m modeling state, not events with a stateflow.
p

Pablichjenkov

11/10/2023, 8:04 PM
Yeup not the best option I agree
Also requires discipline to update the _stateFlow from one entry point only.
👌 1
k

kevin.cianfarini

11/10/2023, 8:08 PM
I think it would be useful for this to be a disclaimer on the documentation and nothing more.
Right now it’s just shared knowledge of those bitten by this
1
b

Bill Phillips

11/10/2023, 8:08 PM
I 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
☝️ 2
a great thing in prod oftentimes, but a bad thing under test
💯 1
p

Pablichjenkov

11/10/2023, 8:12 PM
Badly bitten by this
p

Patrick Steiger

11/12/2023, 2:46 PM
Doesn’t collecting in an unconfined manner ensures you get all values in tests?
k

kevin.cianfarini

11/12/2023, 2:52 PM
I'm not sure, but I don't necessarily want my SUT code running unconfined
r

rkeazor

11/13/2023, 5:56 AM
Hmm but isnt this known during the initial release of Stateflow 🤷. that it can skip values and is distinct until change . If you need something to emit every value while keeping its conflated nature, than use SharedFlow with a replay cache of 1..
d

darkmoon_uk

11/13/2023, 6:50 AM
In most situations isn't it simple to issue a
runCurrent
between value changes to process emissions; probably mimicking the real-world conditions.
d

Dmitry Khalanskiy [JB]

11/13/2023, 10:34 AM
Hi! Here's a relevant discussion from a week ago: https://slack-chats.kotlinlang.org/t/16039814/hey-all-wave-i-have-been-trying-to-test-the-intermediate-sta#9c1503b6-6da5-4e21-9aef-8b03c0dc1dbc The crux of the idea is, in production, if
sf.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?
1
☝️ 1
k

kevin.cianfarini

11/13/2023, 11:34 AM
I think the delay could be a fine solution for most use cases! To know to place the delay between the two value changes still requires that devs know this behavior of state flow exists. Do you think it's valuable to call out on the docs? If so I can make a PR
d

Dmitry Khalanskiy [JB]

11/13/2023, 11:57 AM
But this is written in the docs. https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-state-flow/
Updates 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?
k

kevin.cianfarini

11/13/2023, 12:10 PM
Ah, right. The thing I wanted to clarify here is that
Updates 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
Copy code
channelFlow { ... }.conflate()
So maybe this is a moot point.
I don't use the
Flow.conflate
operator much, but I do use StateFlow rather frequently, so I forgot about that 😄
p

Pablichjenkov

11/13/2023, 12:37 PM
delays have 2 problems inherently. 1- time sensitive, why 25 ms and not 30 ms. Or the type of situation where it works when using 1 sec but not .5 sec. 2- It adds more time to test execution. Some milliseconds here and there when multiplied 10_000 times or so is in the order of minutes. And that matters sometimes, let's say waiting for local Unit Test to open a PR or time cost in a pipeline. I try to avoid delay as much as possible.
d

Dmitry Khalanskiy [JB]

11/13/2023, 12:48 PM
Nope: https://github.com/Kotlin/kotlinx.coroutines/tree/master/kotlinx-coroutines-test#delay-skipping
delay
calls in the test dispatchers are completely deterministic, and add nothing to the test execution time.
1
p

Pablichjenkov

11/13/2023, 1:14 PM
Oh so the delay in the test will make it work without actually waiting! In that case I would say is fine but there are still other corner case, when emitting from multiple threads. How to guarantee 2 events are not emitted at the "same time".
k

kevin.cianfarini

11/13/2023, 1:15 PM
That's locking behavior implemented in state flow, you shouldn't be testing that.
d

Dmitry Khalanskiy [JB]

11/13/2023, 1:17 PM
If you have multiple threads populating one thing (flow, channel, whatever), then how would you ever test this without the test accounting for various possible thread interleavings? And if you do account for multi-threaded interleavings, then you're all clear.
☝️ 2
1
p

Pablichjenkov

11/13/2023, 1:36 PM
What if I have a black box, let's say a ViewModel that exposes a StateFlow, and I want to test an operation that fetches 3 things in parallel and update() the StateFlow. But I want to test that depending on some conditions, sometimes I fetch 2 things, sometimes 3 things per say. Isn't it a valid case to test? Or in general wherever is conflation should not be tested this way
d

Dmitry Khalanskiy [JB]

11/13/2023, 1:58 PM
Tough to say anything definite to such an abstract question. Do you have any specific examples of code that populates a
StateFlow
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.
Filed an issue with an explanation of what we know about testing
StateFlow
without missing any emissions: https://github.com/Kotlin/kotlinx.coroutines/issues/3939
k

kevin.cianfarini

11/13/2023, 2:36 PM
Thanks Dmitry. I’ll leave a comment on the issue
d

Dmitry Khalanskiy [JB]

11/13/2023, 2:37 PM
If you have something to add, then sure, please do! If not, it's not strictly necessary.
💚 1
k

kevin.cianfarini

11/13/2023, 2:38 PM
I’ll read the issue to make sure it doesn’t already touch on what I want to say 🙂
b

Bill Phillips

11/13/2023, 5:32 PM
Thanks for the issue, Dmitry. ❤️
p

Pablichjenkov

11/13/2023, 6:20 PM
Sorry Dmitry for my previous post I was driving. But my use case is pretty simple. I have a ViewModel that exposes a StateFlow. When the screen is open it does several requests to services to bring the data for the different sections. When each section request finishes it updates the StateFlow. I want to test that every time my screen opens, 3 updates to that StateFlow are received. How would I make such a test
k

kevin.cianfarini

11/13/2023, 6:23 PM
I would argue that you shouldn’t really care about the number of network requests, but should instead test that your stateflow ends up in a certain final expected state
p

Pablichjenkov

11/13/2023, 6:28 PM
Ok, that might be an alternative. But just in the case that I want to test the number of updates. Just hypothetically
k

kevin.cianfarini

11/13/2023, 7:30 PM
Have fake network services, each which delay for a different amount of time, and then make assertions in your test based on that.
p

Pablichjenkov

11/13/2023, 7:37 PM
I get it but going back to the thread roots, I agree more detailed docs will definitely help.
g

gildor

11/14/2023, 3:43 AM
@Pablichjenkov But to test it, you should also know order of those 3 updates. Why not add different delay (or even some sync mechanism) to each of those 3 request fake repository implementation, so they will emit in particular order and it would allow to test it even with conflated StateFlow, it's similar to what described in "Fix" section of the GH issue > I want to test that every time my screen opens, 3 updates to that StateFlow are received. Even without test you can say, that it not always will happen on prod, sometimes it will have 2 or 1 update, depedning on how close updates are, so you asking from test what is not true on prod
d

darkmoon_uk

11/14/2023, 3:45 AM
Even if you have 'delay anxiety' in picking a value -
runCurrent
between the
StateFlow
value changes handles it; this isn't a really complex problem just something you have to consider when writing tests.
p

Pablichjenkov

11/14/2023, 5:21 AM
Ok, seems like delay is our best friend in this case. Good
4 Views