Reputation: 6565
I have a viewmodel class that uses a StateFlow that is created using the StateIn operator.
I.E.
private val _state = MutableStateFlow(MyState())
private val myItems = myRepository.myFlow.stateIn(
viewModelScope, SharingStarted.WhileSubscribed(), emptyList<MyObject>())
val state: StateFlow<MyState> = combine(_state, myItems ) { state, myItems ->
..///
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), MyState())
When reading the documentation and various articles and SO questions (which are outdated), I saw that you are supposed to test this by using:
backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
//Collect your flow
}
The problem I am having is that when I create a unit test to test my viewmodel, the collection of the state is not fast enough for the assertions that take place.
@Test
fun checkMyItemTest() = runTest {
val results = mutableListOf<MyState>()
val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.state.collect {
results.add(it)
}
}
viewModel.onEvent(MyEvent.DoSomething())
assert(results[0].myStateParameter) //Perform some check on results[0]
assert(results[1].myStateParameter) //Perform some check on results[1]
job.cancel()
}
The test above fails in the second assertion, since results has only one element in it.
I have gotten it to work by shoving a Thread.sleep
call of 50ms between the two assert calls, but that does not seem like the best approach to me.
@Test
fun checkMyItemTest() = runTest {
val results = mutableListOf<MyState>()
val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.state.collect {
results.add(it)
}
}
viewModel.onEvent(MyEvent.DoSomething())
assert(results[0].myStateParameter) //Perform some check on results[0]
Thread.sleep(50) /// <---- THIS
assert(results[1].myStateParameter) //Perform some check on results[1]
job.cancel()
}
I am not interested in using Turbine at the moment
So I am looking to understand how I can test my flow properly and how I can collect the values emitted by it.
Upvotes: 2
Views: 600
Reputation: 474
You should use the StandardTestDispatcher in place of UnconfinedTestDispathcher
@Test
fun checkMyItemTest() = runTest {
val results = mutableListOf<MyState>()
val job = backgroundScope.launch(StandardTestDispatcher(testScheduler)) {
viewModel.state.collect {
results.add(it)
}
}
viewModel.onEvent(MyEvent.DoSomething())
advanceUntilIdle()
assert(results[0].myStateParameter) //Perform some check on results[0]
assert(results[1].myStateParameter) //Perform some check on results[1]
job.cancel()
}
From the documentation
Remember that UnconfinedTestDispatcher starts new coroutines eagerly, but this doesn’t mean that it’ll run them to completion eagerly as well. If the new coroutine suspends, other coroutines will resume executing.
In this case the collection of viewmodel state. It will collect the default StateFlow value and put to your results
Then when you emit the new value, the coroutine does not wait for that value but assert the result[1]
right away.
Upvotes: 1
Reputation: 402
I am also a newbie to testing but from what I have read runTest{}
skips delay and is not much diffrent from runBlocking so this might work:
@Test
fun checkMyItemTest() = runTest {
val results = Channel<MyState>() // this is a rendevous channel
val job = backgroundScope.launch(UnconfinedTestDispatcher(testScheduler)) {
viewModel.state.collect {
results.send(it)
}
}
viewModel.onEvent(MyEvent.DoSomething())
val result1 = channel.receive() //suspends till it has a corresponding send call
val result2 = channel.receive()
assert(result1.myStateParameter) //Perform some check on result1
assert(result2.myStateParameter) //Perform some check on result2
}
I have not run the code.
Upvotes: 1