hi guys, anyone is a Molecule fan? I'm in trouble ...
# compose
m
hi guys, anyone is a Molecule fan? I'm in trouble writing a test, basically I have modelled a presenter with an initial State
Content
and once it receives the
LoadTracks
event it produces 2 recompositions:
Copy code
> Content
> Loading
> Content (or Error in case of failure)
I'm testing the Failure scenario, but it seems that the
Loading
state is not emitted. It seems an issue related to the test, because the application works properly. Any suggetions? This is my Presenter:
Copy code
class CatalogPresenter @Inject constructor(
    private val mediaRepository: MediaRepository,
) : AbsPresenter<CatalogState, CatalogEvent>() {

    @Composable
    override fun models(events: Flow<CatalogEvent>): CatalogState {
        val state = remember { mutableStateOf<CatalogState>(CatalogState.Content()) }

        LaunchedEffect(Unit) {
            events.collect { event ->
                when (event) {
                    is CatalogEvent.LoadTracks -> loadMedia(state)
                }
            }
        }

        return state.value
    }

    private suspend fun loadMedia(state: MutableState<CatalogState>) {
        state.value = CatalogState.Loading
        state.value = mediaRepository.listMedia().fold(
            ifLeft = { err -> CatalogState.Error(err.message ?: "") },
            ifRight = { list -> CatalogState.Content(list) }
        )
    }

}
And this is my test:
Copy code
private fun testPresenter(events: Flow<CatalogEvent>): Flow<CatalogState> = moleculeFlow(
        mode = RecompositionMode.Immediate
    ) { CatalogPresenter(mediaRepository).models(events = events) }

    @Test
    fun `Test loading upon failure`() = runTest {
        // Given
        val events = flowOf(CatalogEvent.LoadTracks)
        coEvery { mediaRepository.listMedia(any()) } returns Either.Left(AppException.GenericError)

        // When
        testPresenter(events).test {
            // Then
            assertIs<CatalogState.Content>(awaitItem())
            assertIs<CatalogState.Loading>(awaitItem())
            assertIs<CatalogState.Error>(awaitItem())

            coVerify { mediaRepository.listMedia(any()) }
        }
    }
s
What is it that fails here? Also a bit hard to reason about when you start construct the presenter with an events flow which is already pre-filled before the test even starts. Wouldn’t it be nicer to start the presenter, assert the initial state, then pass the event in there, and then continue your assertions?
m
Yes, good questions, I tried both: • passing a flows of event • emit them into a channel like this
Copy code
val events = Channel<CatalogEvent>()
coEvery { mediaRepository.listMedia(any()) } returns Either.Left(AppException.GenericError)

testPresenter(events.receiveAsFlow()).distinctUntilChanged().test {
    // When
    awaitItem() // skip initial
    events.send(CatalogEvent.LoadTracks)

    // Then
    assertIs<CatalogState.Loading>(awaitItem())
    assertIs<CatalogState.Error>(awaitItem())

    coVerify { mediaRepository.listMedia(any()) }
}
but issue is the same
Copy code
assertIs<CatalogState.Loading>(awaitItem())
fails because the actual is
Error
Expected value to be of type <...CatalogState.Loading>, actual <class ...CatalogState$Error>
s
You are doing a back to back change of the state. For the state change to actually be emitted, there should be some things done, like the recomposition clock to tick etc. I don’t know the full details enough myself tbh, but there’s articles etc of how this works. Now you are definitely using the Immediate clock here, so you may expect this to happen immediately, but I’ve always found this to be super hard to reason about, and that’s not super good for tests! Your solution here is to stop using mocks 😊 Make a fake implementation of MediaRepository, and let it suspend while
listMedia
is evaluating. Then assert that it’s loading, then let
listMedia
return the thing, then assert again. A quick example I’ve found here https://github.com/HedvigInsurance/android/blob/9b0270cbc79983d8cd408ba6b250a3391c[…]edvig/android/feature/profile/settings/SettingsPresenterTest.kt, you do your assertion, then you let the service to actually respond to the call (here’s the test implementation) and then you assert when you know the response is back. Nice and simple
m
I'll give a try tnx
FYI: I did a change on the presenter, in this way it works
Copy code
class CatalogPresenter @Inject constructor(
    private val mediaRepository: MediaRepository,
) : AbsPresenter<CatalogState, CatalogEvent>() {

    @Composable
    override fun models(events: Flow<CatalogEvent>): CatalogState {
        var state by remember { mutableStateOf<CatalogState>(CatalogState.Content()) }

        LaunchedEffect(Unit) {
            events.collect { event ->
                state = when (event) {
                    is CatalogEvent.LoadTracks -> CatalogState.Loading
                    is CatalogEvent.UpdateSelectedTrack -> (state as? CatalogState.Content)?.copy(
                        selectedTrackId = event.trackId
                    ) ?: CatalogState.Content()
                }
            }
        }

        LaunchedEffect(state) {
            if (state == CatalogState.Loading) {
                state = loadMedia()
            }
        }

        return state
    }

    private suspend fun loadMedia(): CatalogState {
        return mediaRepository.listMedia().fold(
            ifLeft = { err -> CatalogState.Error(err.message ?: "") },
            ifRight = { list -> CatalogState.Content(list) }
        )
    }
}
basically I invalidate the presenter's state in a different way, because in the previous implementation I invalidate the state adding
Loading
and
Content
in the same tick, and this doesn't produce 2 recomposition, but a single one with the last tick. This explains why I don't receive the
Loading
in my test
s
You changed the presenter functionality quite a lot. Dot you want the network request to be a side effect of changing the state to loading? Now if you already got a loaded state and you want to refresh for example, you're gonna make your UI go back to the loading state, only then to trigger this side effect. But that'd look jarring for the UI. You say "I invalidate the state adding Loading and Content in the same tick" but why should you change your production code to not do this, only because your test isn't written properly to handle such scenarios?
m
Yes good point, probably, I miss some knowledge about how molecule works, because the problem seems not to be on the Test or on the mocking/fake repo doesn't solve anything. The cause is mainly related on how the state is invalidated. I'll continue to investigate on it, a good and struggle way to learn molecule, but I didn't find many good articles about that, and the examples are too simple
s
But what is the problem in the first place? The initial behavior was correct. Se the state as Loading, then wait for the repo to respond with something, then set the state to the new thing. For production you definitely don’t care if this is happening on the same frame, in fact if it did that’s even better, you don’t introduce any artificial delay for no reason. Then for the test, you got the tools at your disposal to emulate whatever scenario you want. And in my links above I show how you can emulate the exact scenario of setting to loading, then the backend responds, then you check on the state again. I would argue that in your test, where the repo responded 100% instantaneously, the expected behavior is in fact for the state to never see the
Loading
state. So I feel like you’re trying to solve an issue that does not exist, only because you don’t want to have more control over how your test runs?
m
Ok, so something is not 100% clear to me, why the
Loading
state should not be emitted if the repo answers instantaneously. Because in my test I would like to have the control of states even if the repo answer sin 0 or 2000ms. this should not impact the unit test.
Or maybe I miss something about the compose runtime
s

https://youtu.be/q9p4ewk-9E4?si=mPrVA2-wf3Vu0S3B&amp;t=1111

Because in my test I would like to have the control of states
That's what I'm saying too. So make that fake, don't make it return anything, and only when you want it to return something let it return that thing. Full control. Which the fake implementation gives you.
1
The hacky (imo) approach to this would be to make sure that you are using the immediate recomposition clock, but at the same time to make sure that you are using an unconfined dispatcher to run your presenter, and that I believe will actually always try and pause and actually emit every single time your mutable state is invalidated. But that's relying on too many abstract things just to make things work. And can be super tricky to reason about in some cases. I myself am not 100% confident explaining how and if that'd work exactly. As opposed to a test reading super simple as: • Initial state is X • Then user does event Y • Then I know that the state is loading • Now the backend has responded with <Response> • Now that the response is received, my state should be Z • And so on...