Marco
10/25/2023, 8:39 AMContent
and once it receives the LoadTracks
event it produces 2 recompositions:
> 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:
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:
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()) }
}
}
Stylianos Gakis
10/25/2023, 2:40 PMMarco
10/25/2023, 2:43 PMval 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
assertIs<CatalogState.Loading>(awaitItem())
fails because the actual is Error
Expected value to be of type <...CatalogState.Loading>, actual <class ...CatalogState$Error>
Stylianos Gakis
10/25/2023, 2:48 PMlistMedia
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 simpleMarco
10/25/2023, 2:53 PMMarco
10/26/2023, 8:22 AMclass 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 testStylianos Gakis
10/26/2023, 8:32 AMMarco
10/26/2023, 9:01 AMStylianos Gakis
10/26/2023, 9:08 AMLoading
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?Marco
10/26/2023, 9:36 AMLoading
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.Marco
10/26/2023, 9:36 AMStylianos Gakis
10/26/2023, 9:45 AMBecause 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.
Stylianos Gakis
10/26/2023, 10:03 AM