JUnit 5 测试 - 无法观察嵌套的 LiveData 值
JUnit 5 Test - Unable to Observe Nested LiveData Value
概述
预期 - 在本地单元测试中保存嵌套的 LiveData 值,然后断言它们的值。
Observed - 在生产代码中成功观察到在 ViewModel 中保存嵌套的 LiveData 值,但在本地单元测试中失败。这可能是由于本地单元测试中缺少线程,而 Android 环境中的 运行。
代码
- ViewModel 包含用户选择要打开的内容时的
LOADING
、CONTENT
和 ERROR
(LCE) 条件。
- 保存 LiveData
NotifyItemChangedEffect
状态以更新视图。
NotifyItemChangedEffect
保存在函数内部以保存发送到视图的内容。 只有在CONTENT
条件下,选择的项目被发送到保存了LiveData对象的视图,ContentToPlay
.
- 在生产中,这与
LOADING
、CONTENT
和 ERROR
期间视图的 UI 更新一起使用,而 ContentToPlay
是 只有在成功的CONTENT
条件下返回。
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(...)))
})
}
}
}
})
...
}
- 设计 -
ContentToPlay
不会在 LOADING
和 ERROR
条件下返回。
- 问题 -
NotifyItemChangedEffect
的嵌套 LiveData 值未保存在单元测试中,这会在每个 LCE 条件下更新视图。此代码在为 ContentToPlay
保存的 LiveData 中执行。此模式已记录并在生产中运行。
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(...))
}
}
}
}
在所有 (LCE) 加载、内容、错误条件下保存 LiveData
在 ViewModel 的所有 LCE 条件中保存 ContentToPlay
LiveData 值或 null
以确保为本地单元测试同步返回值。
注意 - 此策略的缺点是它会向生产代码中的视图发出不必要的 ContentToPlay
值,这并不理想,但看起来并不理想一个主要问题。
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))
}
}
}
})
概述
预期 - 在本地单元测试中保存嵌套的 LiveData 值,然后断言它们的值。
Observed - 在生产代码中成功观察到在 ViewModel 中保存嵌套的 LiveData 值,但在本地单元测试中失败。这可能是由于本地单元测试中缺少线程,而 Android 环境中的 运行。
代码
- ViewModel 包含用户选择要打开的内容时的
LOADING
、CONTENT
和ERROR
(LCE) 条件。 - 保存 LiveData
NotifyItemChangedEffect
状态以更新视图。 NotifyItemChangedEffect
保存在函数内部以保存发送到视图的内容。 只有在CONTENT
条件下,选择的项目被发送到保存了LiveData对象的视图,ContentToPlay
.- 在生产中,这与
LOADING
、CONTENT
和ERROR
期间视图的 UI 更新一起使用,而ContentToPlay
是 只有在成功的CONTENT
条件下返回。
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(...)))
})
}
}
}
})
...
}
- 设计 -
ContentToPlay
不会在LOADING
和ERROR
条件下返回。 - 问题 -
NotifyItemChangedEffect
的嵌套 LiveData 值未保存在单元测试中,这会在每个 LCE 条件下更新视图。此代码在为ContentToPlay
保存的 LiveData 中执行。此模式已记录并在生产中运行。
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(...))
}
}
}
}
在所有 (LCE) 加载、内容、错误条件下保存 LiveData
在 ViewModel 的所有 LCE 条件中保存 ContentToPlay
LiveData 值或 null
以确保为本地单元测试同步返回值。
注意 - 此策略的缺点是它会向生产代码中的视图发出不必要的 ContentToPlay
值,这并不理想,但看起来并不理想一个主要问题。
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))
}
}
}
})