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

Shashank

09/13/2020, 5:57 PM
Hey everyone! I am trying to test my 
StateFlow
 by asserting the list of values received by it.
Copy code
data class State(val isLoading: Boolean, val name: String?)

fun main() {
    val mutableStateFlow = MutableStateFlow(State(false, null))
    val stateFlow = mutableStateFlow as StateFlow<State>

    val scope1 = CoroutineScope(Job())
    val scope2 = CoroutineScope(Job())

    scope1.launch {
        delay(1000)
        mutableStateFlow.value = State(true, null)
        mutableStateFlow.value = State(false, "name")
    }

    scope2.launch {
        stateFlow.collect {
            println("value = $it")
        }
    }
    Thread.sleep(2000)
}
Here is my code. Instead of getting 
State(true, null)
 as the second value, I only receive the initial value and the last value (
State(false, name)
)
a

araqnid

09/13/2020, 6:00 PM
I found sticking a
delay(10)
between the two writes to
mutableStateFlow.value
showed the intermediate state. Presumably it’s a race condition, the intermediate state is being overwritten before it’s been published
in fact, even just a
yield()
is enough on my test machine
z

Zach Klippenstein (he/him) [MOD]

09/13/2020, 6:07 PM
There are no suspending calls between setting the two states, so they are immediately set one-after-the-other, before your collector has a chance to resume.
StateFlow
conflates such values, so the intermediate value will never be seen. Steve’s suggestions add a suspend point between the two calls, which gives your collector a chance to resume.
To test this sort of thing, I usually use an explicit trigger to make the test more explicit. Something like:
Copy code
val finishLoad = Job()
…
mutableStateFlow.value = State(true, null)
finishLoad.join()
mutableStateFlow.value = State(false, "name")
…
assertEquals(State(true, null), collectingChannel.receive())
assertTrue(collectingChannel.isEmptyForReceive)
finishLoad.complete()
assertEquals(State(false, "name"), collectingChannel.receive())
assertTrue(collectingChannel.isEmptyForReceive)
a

Adam Powell

09/13/2020, 6:16 PM
perhaps higher level, when working with observable state it should never matter for an observer's correctness if it doesn't see intermediate states. (If you're just trying to test that your emitter is performing all of the updates it should, that's another matter)
☝️ 2
s

Shashank

09/13/2020, 7:06 PM
So I am replacing my Behaviour Subject with StateFlow. And I find this behaviour (heh) very undesirable. I don’t think StateFlow should conflate the values. Instead, it should be left to the consumer. What if I want to show something to the user when I receive the exact same state updates. @Adam Powell Yes I do want to verify if my emitter is performing all the updates. I am using an MVI pattern and so I do want to verify the intermediate states as well. I do agree that sticking a 10 milli delay does “fix” this but I was thinking if there is a better way?
z

Zach Klippenstein (he/him) [MOD]

09/13/2020, 8:33 PM
I don't think adding a delay fixes anything, it's a hack that hides the core issue, which is probably that the data you're using this for isn't really state but events. State, by nature, doesn't matter if you conflate it. If you start out in state A, then end up in state C, it doesn't matter if there was a state B in the middle there somewhere - the final state is still C. StateFlow is meant for state, and so it makes assumptions like this which are only valid for state. There is another type, SharedFlow, which hasn't been released yet but is suitable for event data where every emission is meaningful. It does not conflate items itself. Unfortunately, it is only scheduled for the 1.4 release of coroutines, and there has been no communication on when that is going to happen. Until it is released, your best option is to use BroadcastChannel for events.
5
s

Shashank

09/13/2020, 10:35 PM
State, by nature, doesn’t matter if you conflate it. If you start out in state A, then end up in state C, it doesn’t matter if there was a state B in the middle there somewhere
Well, doesn’t this make testing a bit difficult? Let’s say I set
state = State(false, emptyList)
. Then I call a function which sets
state = State(true, emptyList)
. The state flow will only give me the second value if there is very little time difference between the two. So wouldn’t I need to know about the implementation of my SUT and know at which points is the state set without any dealy?
your best option is to use BroadcastChannel for events.
I don’t think it re-emits the latest value to new subscribers. So I can’t really use it for my view state stream.
a

Adam Powell

09/13/2020, 11:06 PM
no, it tends to make testing easier. State observers should always be idempotent and any one state snapshot from the stream should be sufficient to produce a correct result. For cases where you are testing a state emitter's precise behavior you can collect the flow using the unconfined dispatcher.
☝️ 1
z

Zach Klippenstein (he/him) [MOD]

09/13/2020, 11:35 PM
I think unit tests generally should only test individual state transitions (otherwise you're not really writing a unit test). If you do want to test multiple transitions for some reason, then you should have some way to explicitly trigger transitions like I gave an example for above.
a

Anvith

09/14/2020, 3:49 AM
@Adam Powell Expecting idempotent behaviour from downstream listeners is a different thing than processing/emitting all states in the current stream don't you think? This seems like a slippery slope for making assumptions on what downstream listeners should/shouldn't receive.
a

Adam Powell

09/14/2020, 1:48 PM
In the case of state the two are related. Only the current value matters, whether you receive that value multiple times or if it changes several times before you sample it. Could you elaborate on the slippery slope part?
a

Anvith

09/15/2020, 5:10 AM
Isn't the API contract an emission contract as well ( i.e. i send stuff you store it and send it to others) not just a persistence contract? I think when you speak purely of state may be what you say is true however when we talk of propagation of state I feel making this assumption seems like short circuiting. Regarding this`Could you elaborate on the slippery slope` Anything particular you were seeking since I was pointing to StateFlow making assumptions on emission sequence. For e.g. some downstream frameworks could be stateful( eg. animations) and rely not just on an absolute state but also on the transition(something which is a sideeffect/computed property of succesive state emissions).
7 Views