Issue
Overview
Expected - Saving nested LiveData values in a local unit test, and then asserting their values.
Observed - Saving nested LiveData values in a ViewModel are observed successfully in production code, but fail in the local unit test. This is potentially due to the lack of threading in local unit tests vs. running on the Android environment.
Code
- ViewModel contains
LOADING,CONTENT, andERROR(LCE) conditions for when a user selects content to open. - The LiveData
NotifyItemChangedEffectstate is saved in order to update the view. NotifyItemChangedEffectis saved inside of the function to save content sent to the view. Only in theCONTENTcondition, the item selected is sent to the view with a LiveData object saved,ContentToPlay.- In production, this works with the view's UI updating during
LOADING,CONTENT, andERROR, whileContentToPlayis only returned in the successfulCONTENTcondition.
ContentViewModel.kt
is ContentSelected -> {
_feedViewState.value = _feedViewState.value?.copy(
// LiveData value for ContentToPlay initiated here.
contentToPlay = switchMap(getAudiocast(contentSelected)) { lce ->
liveData {
when (lce) {
is Loading ->
_viewEffect.value = _viewEffect.value?.copy(
notifyItemChanged = liveData {
emit(Event(NotifyItemChangedEffect(...)))
})
is Lce.Content -> {
_viewEffect.value = _viewEffect.value?.copy(
notifyItemChanged = liveData {
emit(Event(NotifyItemChangedEffect(...)))
})
// LiveData value for ContentToPlay saved here.
emit(Event(lce.packet))
}
is Error -> {
_viewEffect.value = _viewEffect.value?.copy(
notifyItemChanged = liveData {
emit(Event(NotifyItemChangedEffect(...)))
})
_viewEffect.value = _viewEffect.value?.copy(
snackBar = liveData {
emit(Event(SnackBarEffect(...)))
})
}
}
}
})
...
}
- By design -
ContentToPlayis not returned in theLOADINGandERRORconditions. - Issue - The nested LiveData values for
NotifyItemChangedEffectare not saved in the unit test, which update the view in each LCE condition. This code is executed inside of the LiveData saved forContentToPlay. This pattern is logged and working in production.
PlayContentTests.kt
@ExtendWith(InstantExecutorExtension::class)
class PlayContentTests {
@ParameterizedTest
@MethodSource("FeedLoad")
fun `Play Content`(test: PlayContentTest) = runBlocking {
// ViewModel method included to initiate ContentSelected event.
...
when (test.lceState) {
LOADING ->
assertThat(contentViewModel.viewEffect.getOrAwaitValue().notifyItemChanged.getOrAwaitValue().peekEvent()).isEqualTo(
NotifyItemChangedEffect(...))
CONTENT -> {
assertThat(contentViewModel.feedViewState.getOrAwaitValue().contentToPlay.getOrAwaitValue().peekEvent()).isEqualTo(
ContentToPlay(...))
assertThat(contentViewModel.viewEffect.getOrAwaitValue().notifyItemChanged.getOrAwaitValue().peekEvent()).isEqualTo(
NotifyItemChangedEffect(...))
}
ERROR -> {
assertThat(contentViewModel.feedViewState.getOrAwaitValue().contentToPlay.getOrAwaitValue().peekEvent()).isEqualTo(
ContentToPlay(...))
assertThat(contentViewModel.viewEffect.getOrAwaitValue().notifyItemChanged.getOrAwaitValue().peekEvent()).isEqualTo(
NotifyItemChangedEffect(...))
}
}
}
}
Solution
Saving LiveData in all (LCE) Loading, Content, Error conditions
Saving a ContentToPlay LiveData value or null in all LCE conditions of the ViewModel to ensure a value is returned synchronously for local unit testing.
Note - The downside of this strategy is it emits a ContentToPlay value unnecessarily to the view in production code which is not ideal, but does not appear to be a major issue.
ContentViewModel.kt
_feedViewState.value = _feedViewState.value?.copy(contentToPlay =
switchMap(getAudiocast(contentSelected)) { lce ->
liveData {
when (lce) {
is Loading -> {
setContentLoadingStatus(contentSelected.content.id, View.VISIBLE)
_viewEffect.value = _viewEffect.value?.copy(
notifyItemChanged = liveData {
emit(Event(NotifyItemChangedEffect(contentSelected.position)))
})
// Empty ContentToPlay saved.
emit(Event(null))
}
is Lce.Content -> {
setContentLoadingStatus(contentSelected.content.id, View.GONE)
_viewEffect.value = _viewEffect.value?.copy(
notifyItemChanged = liveData {
emit(Event(NotifyItemChangedEffect(contentSelected.position)))
})
// ContentToPlay saved.
emit(Event(lce.packet))
}
is Error -> {
setContentLoadingStatus(contentSelected.content.id, View.GONE)
_viewEffect.value = _viewEffect.value?.copy(
notifyItemChanged = liveData {
emit(Event(NotifyItemChangedEffect(contentSelected.position)))
})
if (lce.packet.filePath.equals(TTS_CHAR_LIMIT_ERROR))
_viewEffect.value = _viewEffect.value?.copy(
snackBar = liveData {
emit(Event(SnackBarEffect(TTS_CHAR_LIMIT_ERROR_MESSAGE)))
})
else _viewEffect.value = _viewEffect.value?.copy(
snackBar = liveData {
emit(Event(SnackBarEffect(CONTENT_PLAY_ERROR)))
})
// Empty ContentToPlay saved.
emit(Event(null))
}
}
}
})
Answered By - Adam Hurwitz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.