tomerpacific
tomerpacific

Reputation: 6565

Unit Testing StateFlow Created By StateIn

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

Answers (2)

Dennis Nguyen
Dennis Nguyen

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

Purple6666
Purple6666

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

Related Questions