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

Shashank

11/05/2023, 9:04 PM
Hey all! 👋 I have been trying to test the intermediate states for StateFlow. But since my ViewModel has 2 emissions in the init block, I only receive the latest state
(isLoading=false…)
while testing. Any ideas how to receive both the states? Code in thread.
ViewModel
Copy code
class TestingViewModel : ViewModel() {

    data class ViewState(
        val isLoading: Boolean = false,
        val data: String? = null,
    )

    private val _state: MutableStateFlow<ViewState> = MutableStateFlow(ViewState())
    val state: StateFlow<ViewState> = _state.asStateFlow()


    init {
        _state.value = state.value.copy(isLoading = true)
        _state.value = state.value.copy(isLoading = false, data = "hello!")
    }
}
Test
Copy code
@Test
fun test() = runTest {
    val vm = TestingViewModel()
    vm.state.test {
        println(awaitItem())
        println(awaitItem())
    }
}
j

jw

11/05/2023, 9:26 PM
Should be impossible
s

Shashank

11/05/2023, 9:27 PM
😲 so what should I do to test this? Change my ViewModel’s code?
j

jw

11/05/2023, 9:29 PM
What is your goal with the test? As far as the outside of the view model is concerned the difference between starting with that value and emitting it with two values is indecipherable and implementation detail.
s

Shashank

11/05/2023, 9:30 PM
My goal is to make sure the states are emitted in a certain order. Is that an incorrect way to testing VMs? What would you suggest?
A more general question would be: how should I be testing my VMs?
j

jw

11/05/2023, 10:22 PM
If your view model produces two states or fifty or just one in init then from the outside it will always appear as only one
s

streetsofboston

11/05/2023, 10:23 PM
Order should not be important. You should test, assert, the end-state, given a start-state and given an event (or method call) from your VM.
s

Shashank

11/05/2023, 10:31 PM
You should test, assert, the end-state, given a start-state
So all the VM tests should just assert the last state? And never a transition of states?
If your view model produces two states or fifty or just one in init then from the outside it will always appear as only one
What if there is an actual delay in the init block? Then I will receive both events To both: What I am trying to get at is that I want my test to be like: “On VM init, a load state is shown and then the data is presented”. But the conflation messes it up and I need to be wary of that. I am curious how you guys ever faced this and how you handled it? Or maybe I am testing the VM in a wrong way like Anton said and I should just be concerned with the final state.
j

jw

11/05/2023, 10:50 PM
Init blocks are synchronous. You cannot delay in anyway that someone can observe your object halfway initialized
3
c

Chris Fillmore

11/05/2023, 11:03 PM
If you want, you could have your class publish an event (via eg SharedFlow). If the consumer of your class needs to know about loading events, that’s the way to do it. But the consumer of your class (in this case your test), cannot depend on on loading state, because regardless of implementation (StateFlow or otherwise), there is no guarantee your class will ever be in the loading state.
c

Casey Brooks

11/05/2023, 11:19 PM
StateFlows, by design, conflate their values because they represent state, and the only state that really matters is the current one. If managing individual emissions is important, that’s what SharedFlow is for. In general, when testing your VMs, you’ll get more reliable tests if you treat the VM like a black box and don’t attempt to verify the internal workings. All that matters is that when you request some change, you resulting state is what you expect, since that’s how the UI treats the VM as well.
If you do need to track the order of every State change, that will need to be done separately from the StateFlow itself. My Ballast MVI library has a test module which does accurately record multiple state changes like the original question, but it does this my having the internal StateFlow protected so the library can send notifications of the change to a SharedFlow whenever the state does change. It requires a bit more boilerplate and some slightly different ways of thinking about what your VM is doing, but it can be very effective if you want to have a rigorous VM where you can be absolutely certain that everything is being processed exactly like you expect.
👍 1
d

Dmitry Khalanskiy [JB]

11/06/2023, 9:19 AM
Why would you want to test behavior that can never occur in production code?
s

Shashank

11/06/2023, 9:40 AM
It can occur in production. Let’s replace the code in init block with this
Copy code
_state.value = state.value.copy(isLoading = true)
val data = repo.getUser() //suspend function
        _state.value = state.value.copy(isLoading = false, data = data)
So the UI will see 2 states, one for loading and then the data state.
d

Dmitry Khalanskiy [JB]

11/06/2023, 9:45 AM
With the initial code you provided, it can't. The new code is entirely different and easy to test: just mock the repository with
Copy code
suspend fun getUser(): User {
  delay(20.milleseconds) // a network call
  return createFakeUser()
}
s

Shashank

11/06/2023, 9:45 AM
Correct. But that does mean I am having to add a delay myself to test the intermediate state.
d

Dmitry Khalanskiy [JB]

11/06/2023, 9:46 AM
Yes. What's the issue with that? You are testing the behavior for the case when there's a delay between the first assignment and the second one. Why shouldn't the test reflect exactly that?
s

Shashank

11/06/2023, 9:48 AM
Hmm okay. Maybe I have been doing it wrong I guess. I never add delays in my fakes.
d

Dmitry Khalanskiy [JB]

11/06/2023, 9:51 AM
This really depends on what you're testing. • If you want to ensure the correct behavior in case of a noticeable network delay... there should be some emulation of the network delay. • If you want to ensure that the behavior is sensible even when the data is available seemingly instantly, there shouldn't be.
runTest
will automatically skip delays (except in code forked off to other dispatchers, like
<http://Dispatchers.IO|Dispatchers.IO>
or custom thread pools), so you can even write
delay(400.hours)
without causing your tests to slow down or become non-deterministic.
s

Shashank

11/06/2023, 9:51 AM
Makes sense, thank you! 🙏
🙂 1
5 Views