kotlinx.coroutines icon indicating copy to clipboard operation
kotlinx.coroutines copied to clipboard

kotlinx-coroutines-test:1.6.1- Issue with collecting values of StateFlow in tests

Open Subuday opened this issue 3 years ago • 0 comments
trafficstars

There are collected only the first and the last values. It's an Android application. mainDispatcherRule is used to replace Dispatcher.Main -> UnconfinedTestDispatcher. The idea is to collect all emitted values from StateFlow. But the intermediate values are skipped. I can not grab the problem. I've referenced to this implementation

@ExperimentalCoroutinesApi
class BaseFeatureTest {

    @get:Rule
    val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun `states and news are emitted correctly`() = runTest {
        val sut = createBaseFeature()
        val values = mutableListOf<FakeState>()
        val job = launch(UnconfinedTestDispatcher(testScheduler)) {
            sut.collect {
                values += it
            }
        }

        sut.emit(FakeWish.Increment)

        println(values) // [FakeState(counter=100), FakeState(counter=102)]
        val expectedValues = listOf(FakeState(100), FakeState(101), FakeState(102))
        assertContentEquals(expectedValues, values)

        job.cancel()
        sut.cancel()
    }

    private fun createBaseFeature() = BaseFeature(
        initialState,
        FakeActor(),
        FakeReducer(),
        FakeNewsPublisher()
    )
}
data class FakeState(val counter: Int)
sealed interface FakeWish {
    object Increment : FakeWish
}
sealed interface FakeEffect {
    object TestEffect : FakeEffect
}
sealed interface FakeNews

class FakeActor : Actor<FakeState, FakeWish, FakeEffect> {

    override fun invoke(state: FakeState, wish: FakeWish): Flow<FakeEffect> {
        return when(wish) {
            is FakeWish.Increment -> flowOf(FakeEffect.TestEffect, FakeEffect.TestEffect)
        }
    }
}

class FakeReducer : Reducer<FakeState, FakeEffect> {

    override fun invoke(state: FakeState, effect: FakeEffect): FakeState {
        return when(effect) {
            is FakeEffect.TestEffect -> {
                state.copy(counter = state.counter + 1)
            }
        }
    }
}

class FakeNewsPublisher : NewsPublisher<FakeWish, FakeEffect, FakeState, FakeNews> {

    override fun invoke(wish: FakeWish, effect: FakeEffect, state: FakeState): FakeNews? {
        return null
    }
}

val initialState = FakeState(counter = 100)
open class BaseFeature<Wish : Any, Effect : Any, State : Any, News : Any>(
    initialState: State,
    private val actor: Actor<State, Wish, Effect>,
    private val reducer: Reducer<State, Effect>,
    private val newsPublisher: NewsPublisher<Wish, Effect, State, News>
) : Flow<State>, FlowCollector<Wish>, ViewModel() {

    private val stateFlow = MutableStateFlow(initialState)
    private val wishChannel = Channel<Wish>()

    private val _newsChannel = Channel<News>()
    val news = _newsChannel.receiveAsFlow()

    private val collectJobWish: Job

    init {
        collectJobWish = wishChannel
            .receiveAsFlow()
            .onEach { wish -> processWish(stateFlow.value, wish) }
            .launchIn(viewModelScope)
    }

    override suspend fun collect(collector: FlowCollector<State>) {
        stateFlow.collect(collector)
    }

    override suspend fun emit(value: Wish) {
        wishChannel.send(value)
    }

    fun cancel() {
        viewModelScope.cancel()
    }

    private suspend fun processWish(state: State, wish: Wish) {
        coroutineScope {
            actor(state, wish)
                .onEach { effect -> processEffect(stateFlow.value, wish, effect) }
                .launchIn(this)
        }
    }

    private suspend fun processEffect(state: State, wish: Wish, effect: Effect) {
        val newState = reducer(state, effect)
        stateFlow.value = newState
        publishNews(wish, effect, newState)
    }

    private suspend fun publishNews(wish: Wish, effect: Effect, state: State) {
        newsPublisher.invoke(wish, effect, state)?.let {
            _newsChannel.send(it)
        }
    }
}

Subuday avatar Jul 14 '22 18:07 Subuday