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

Berkeli Alashov

06/27/2022, 6:11 AM
Hello. I'm having trouble testing a simple ViewModel that exposes a state by combining MutableStateflows. Updating MutableStateflow value doesn't cause combined state to emit new value during testing (while works as expected when manually testing on device) Code with failing test comments in the thread🧵
Copy code
class StateFlowTestViewModel : ViewModel() {
    data class State(val data: Boolean? = null, val isSaving: Boolean = false, val error: Throwable? = null) {
        val isSaveEnabled = data != null && !isSaving

        companion object {
            val Empty = State()
        }
    }

    @VisibleForTesting val dataState = MutableStateFlow<Boolean?>(null)
    @VisibleForTesting val isSavingState = MutableStateFlow<Boolean>(false)
    @VisibleForTesting val errorState = MutableStateFlow<Throwable?>(null)

    val state = combine(dataState, isSavingState, errorState, ::State).stateIn(viewModelScope, SharingStarted.Lazily, State())

    fun setData(data: Boolean?) {
        dataState.value = data
        // also tried .update { data}, .emit, .tryEmit with viewModelScope.launch
    }

    fun save() = viewModelScope.launch {
        with(state.value) {
            runCatching {
                require(isSaveEnabled) { "Save is not enabled, is UI out of sync?" }
            }.onFailure {
                errorState.value = it
                return@launch
            }

            isSavingState.value = true
            // delay(100)
            isSavingState.value = false
            errorState.value = Throwable("TestError")
        }
    }
}
Copy code
@RunWith(AndroidJUnit4::class)
class StateFlowTestViewModelTest  {

    @get:Rule
    val instantTaskExecutorRule = InstantTaskExecutorRule()

    @Test
    fun test_setDataAndSave() = runTest {
        val viewModel = StateFlowTestViewModel()

        viewModel.setData(true)

        assertThat(viewModel.dataState.value)
            .isTrue() // doesn't fail

        // assertThat(viewModel.state.value.data)
        //     .isTrue() // fails

        assertThat(viewModel.state.first().data)
            .isTrue() // doesn't fail.
        // ^ calling viewModel.state.first() is also somehow required for below viewModel.save call
        // so it doesn't fail with ui out of sync error (bcz viewModel.state.value.data is still null &  viewModel.state.value.isSaveEnabled is false)

        viewModel.save()
        // also tried viewModel.save().join()

        assertThat(viewModel.errorState.value)
            .isNotNull() // doesn't fail

        assertThat(viewModel.state.value.error)
            .isNotNull() // fails bcz still null

        // also tried
        // assertThat(viewModel.state.first().error).isNotNull()
        // viewModel.state.test {
        //     assertThat(awaitItem().error)
        //         .isNotNull()
        // }
    }
}
b

Berkeli Alashov

06/27/2022, 5:55 PM
I will try that, thanks! I've seen that before but for some reason assumed it was being handled by
runTest
& Roboelectric
Things are mostly working now. Additionally I had to use
state.first()
instead of
state.value
in
vm.save()
to read latest set values right after calling
vm.setData(true)
in tests. Do you know why that is?
33 Views