我已经使用Android体系结构组件和Reactive方法构建了一个启动画面。
我从首选项LiveData对象fun isFirstLaunchLD(): SharedPreferencesLiveData<Boolean>
返回。
我有将LiveData传递到视图并更新“首选项”的ViewModel
val isFirstLaunch = Transformations.map(preferences.isFirstLaunchLD()) { isFirstLaunch ->
if (isFirstLaunch) {
preferences.isFirstLaunch = false
}
isFirstLaunch
}
在我的片段中,我观察了ViewModel中的LiveData
viewModel.isFirstLaunch.observe(this, Observer { isFirstLaunch ->
if (isFirstLaunch) {
animationView.playAnimation()
} else {
navigateNext()
}
})
我现在想测试我的ViewModel来查看isFirstLaunch是否正确更新。我该如何测试?我是否正确分离了所有图层?您将在此示例代码上进行什么样的测试?
答案 0 :(得分:2)
我是否正确分离了所有图层?
这些层似乎合理地分开了。逻辑在ViewModel中,您not referring to storing Android Views/Fragments/Activities in the ViewModel。
您将在此示例代码上进行什么样的测试?
在测试ViewModel时,您可以在此代码上编写检测或纯单元测试。对于单元测试,您可能需要弄清楚如何对首选项进行双重测试,以便可以专注于isFirstLaunch / map行为。一种简单的方法是将一个虚假的偏好测试双重传递给ViewModel。
我该如何测试?
我在测试LiveData转换时写了一些内容,继续阅读!
Tl; DR 您可以测试LiveData转换,只需确保已观察到转换的结果 LiveData。
事实1:如果未观察到,LiveData不会发出数据。 LiveData的“ lifecycle awareness”是为了避免额外的工作。 LiveData知道其观察者(通常是“活动/片段”)所处的生命周期状态。这使LiveData可以知道屏幕上实际是否观察到它。如果未观察到LiveData或观察者不在屏幕上,则不会触发观察者(不调用观察者的onChanged方法)。这很有用,因为它使您不必进行额外的工作,例如“更新/显示”屏幕外的片段。
事实2:必须观察到由Transformations生成的LiveData才能触发转换。要触发Transformation,必须观察结果LiveData(在本例中为isFirstLaunch)。同样,在没有观察的情况下,不会触发LiveData观察器,并且也不会触发转换。
在对ViewModel进行单元测试时,不应具有或需要访问Fragment / Activity。如果您无法以通常的方式设置观察者,那么如何进行单元测试?
事实3:在测试中,您不需要LifecycleOwner来观察LiveData,可以使用observeForever 。您不需要生命周期观察者就可以测试LiveData。这很令人困惑,因为通常在测试之外(即,在生产代码中),您将使用LifecycleObserver,例如Activity或Fragment。
在测试中,您可以将LiveData方法observeForever()用于没有生命周期所有者的观察者。由于没有LifecycleOwner,因此该观察者“总是”在观察并且没有开/关屏幕的概念。因此,您必须使用removeObserver(observer)手动删除观察者。
将所有内容放在一起,您可以使用observeForever测试您的转换代码:
class ViewModelTest {
// Executes each task synchronously using Architecture Components.
// For tests and required for LiveData to function deterministically!
@get:Rule
val rule = InstantTaskExecutorRule()
@Test
fun isFirstLaunchTest() {
// Create observer - no need for it to do anything!
val observer = Observer<Boolean> {}
try {
// Sets up the state you're testing for in the VM
// This affects the INPUT LiveData of the transformation
viewModel.someMethodThatAffectsFirstLaunchLiveData()
// Observe the OUTPUT LiveData forever
// Even though the observer itself doesn't do anything
// it ensures any map functions needed to calculate
// isFirstLaunch will be run.
viewModel.isFirstLaunch.observeForever(observer)
assertEquals(viewModel.isFirstLaunch.value, true)
} finally {
// Whatever happens, don't forget to remove the observer!
viewModel.isFirstLaunch.removeObserver(observer)
}
}
}
一些注意事项:
androidx.arch.core:core-testing:<current-version>
才能使用此规则。observeForever
,但有时它也会进入生产代码。请记住,在生产代码中使用observeForever
时,您会失去生命周期意识的好处。您还必须确保不要忘记删除观察者!最后,如果您编写了很多这样的测试,那么try,catch-catch-remove-code可能会变得乏味。如果您使用的是Kotlin,则可以创建扩展功能,以简化代码并避免忘记删除观察者的可能性。有两种选择:
选项1
/**
* Observes a [LiveData] until the `block` is done executing.
*/
fun <T> LiveData<T>.observeForTesting(block: () -> Unit) {
val observer = Observer<T> { }
try {
observeForever(observer)
block()
} finally {
removeObserver(observer)
}
}
这会使测试看起来像这样:
class ViewModelTest {
@get:Rule
val rule = InstantTaskExecutorRule()
@Test
fun isFirstLaunchTest() {
viewModel.someMethodThatAffectsFirstLaunchLiveData()
// observeForTesting using the OUTPUT livedata
viewModel.isFirstLaunch.observeForTesting {
assertEquals(viewModel.isFirstLaunch.value, true)
}
}
}
选项2
@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
time: Long = 2,
timeUnit: TimeUnit = TimeUnit.SECONDS,
afterObserve: () -> Unit = {}
): T {
var data: T? = null
val latch = CountDownLatch(1)
val observer = object : Observer<T> {
override fun onChanged(o: T?) {
data = o
latch.countDown()
this@getOrAwaitValue.removeObserver(this)
}
}
this.observeForever(observer)
try {
afterObserve.invoke()
// Don't wait indefinitely if the LiveData is not set.
if (!latch.await(time, timeUnit)) {
throw TimeoutException("LiveData value was never set.")
}
} finally {
this.removeObserver(observer)
}
@Suppress("UNCHECKED_CAST")
return data as T
}
这会使测试看起来像这样:
class ViewModelTest {
@get:Rule
val rule = InstantTaskExecutorRule()
@Test
fun isFirstLaunchTest() {
viewModel.someMethodThatAffectsFirstLaunchLiveData()
// getOrAwaitValue using the OUTPUT livedata
assertEquals(viewModel.isFirstLaunch.getOrAwaitValue(), true)
}
}
答案 1 :(得分:0)
这取决于您的 SharedPreferencesLiveData 做什么。
如果SharedPreferencesLiveData包含Android特定类,则您将无法正确测试它,因为JUnit无法访问Android特定类。
另一个问题是,要观察LiveData,您需要某种 Lifecycle 所有者。 (原始邮政编码中的 this 。)
在单元测试中,可以简单地将“ this ”替换为以下内容:
private fun lifecycle(): Lifecycle {
val lifecycle = LifecycleRegistry(Mockito.mock(LifecycleOwner::class.java))
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
return lifecycle
}
然后以以下方式使用:
@RunWith(MockitoJUnitRunner::class)
class ViewModelTest {
@Rule
@JvmField
val liveDataImmediateRule = InstantTaskExecutorRule()
@Test
fun viewModelShouldLoadAttributeForConsent() {
var isLaunchedEvent: Boolean = False
// Pseudo code - Create ViewModel
viewModel.isFirstLaunch.observe(lifecycle(), Observer { isLaunchedEvent = it } )
assertEquals(true, isLaunchedEvent)
}
private fun lifecycle(): Lifecycle {
val lifecycle = LifecycleRegistry(Mockito.mock(LifecycleOwner::class.java))
lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
return lifecycle
}
}
注意:您必须具有规则,以便LiveData可以立即执行,而不是在需要时执行。