jean
08/18/2020, 10:54 AMviewModelScope
to be able to use internal suspend function, and then use lifeCycleScope
to observe the state flow variable. That leave us with two different coroutines, doesn’t it? Now I’m trying to test my code that use a similar approach and run in the situation where one of the mocked object used from the viewModelScope
always return null regardless of the whenever
mocking configuration.
I stoped using the init
block of my view model to jump start the handling of intent and replace it by a suspend fun, so now my view calls that function which returns the state flow to observe instead of the variable. This solved my null-value-mocked issue.
Is that a better way way? Is there anything I’m not understanding?Gabriel Feo
08/18/2020, 11:34 AMjean
08/18/2020, 11:47 AMjean
08/18/2020, 11:47 AMclass SearchViewModel(
private val flow: Flow<SearchInputs>,
private val searchRepository: SearchRepository
) : ViewModel() {
private val initial = SearchFragmentState(
isLoading = false,
result = null,
error = null
)
private val _state = MutableStateFlow(initial)
suspend fun observeStateChange(): StateFlow<SearchFragmentState> {
flow.transform { input ->
when (input) {
is SearchInputs.Search -> {
emit(SearchOutputs.Loading)
val output = search(input.organizationId, input.query)
emit(output)
}
}
}
.mapToState()
return _state
}
private suspend fun search(organizationId: String, query: String): SearchOutputs {
return when (val data = searchRepository.search(organizationId, query)) {
is Either.Right -> SearchOutputs.SearchResult(data.value)
is Either.Left -> SearchOutputs.SearchFail(data.value)
null -> SearchOutputs.SearchFail()
}
}
private suspend fun Flow<SearchOutputs>.mapToState() {
collect { output ->
when (output) {
is SearchOutputs.Loading -> {
_state.value = SearchFragmentState(
isLoading = true,
result = null,
error = null
)
}
is SearchOutputs.SearchResult -> {
_state.value = SearchFragmentState(
isLoading = false,
result = output.data,
error = null
)
}
is SearchOutputs.SearchFail -> {
_state.value = SearchFragmentState(
isLoading = false,
result = null,
error = output.error ?: UnexpectedException
)
}
}
}
}
}
jean
08/18/2020, 11:49 AM@ExperimentalTime
@Test
fun `should search and success`() = coroutineTestRule.runBlockingTest {
val flow = flowOf(SearchInputs.Search("test-as", "test"))
val viewModel = SearchViewModel(flow, searchRepository)
whenever(searchRepository.search("test-as", "test"))
.thenReturn(Either.Right(data))
viewModel.observeStateChange().test {
var state = expectItem()
assertFalse(state.isLoading)
assertNull(state.result)
assertNull(state.error)
state = expectItem()
assertTrue(state.isLoading)
assertNull(state.result)
assertNull(state.error)
state = expectItem()
assertFalse(state.isLoading)
assertEquals(data, state.result)
assertNull(state.error)
expectComplete()
}
}
I don’t have the null value problem anymore ( searchRepository.search
was returning null when I was using both scopes)jean
08/18/2020, 11:50 AMexpectItem()
the first time I’m expecting the state equivalent to the SearchOutputs.Loading
but it is skipped, and I get the state equivalent to SearchOutputs.SearchResult
insteadjean
08/18/2020, 11:57 AM_state.value = SearchFragmentState(
isLoading = false,
result = output.data,
error = null
)
inside the branch for SearchOutputs.SearchResult
inside mapToState()
then the test receives the state corresponding to SearchOutputs.Loading
. So it feels like the sub-flow is emitting only it’s last valuejean
08/18/2020, 1:48 PMviewModelScope
and the mocking of calls within that scope. So back to the first issue. I have to say that if I run the app, the view receives each states as intended