Android 测试LiveData转换?

Android 测试LiveData转换?,android,android-testing,android-architecture-components,android-livedata,android-jetpack,Android,Android Testing,Android Architecture Components,Android Livedata,Android Jetpack,我已经使用Android架构组件和反应式方法构建了一个启动屏幕。 我从首选项LiveData对象返回乐趣isFirstLaunchLD():SharedReferencesLiveData。 我有一个ViewModel,它将LiveData传递给视图并更新首选项 val isFirstLaunch = Transformations.map(preferences.isFirstLaunchLD()) { isFirstLaunch -> if (isFirstLaunch) {

我已经使用Android架构组件和反应式方法构建了一个启动屏幕。 我从首选项LiveData对象返回乐趣isFirstLaunchLD():SharedReferencesLiveData。 我有一个ViewModel,它将LiveData传递给视图并更新首选项

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是否正确更新。我如何测试它?我是否正确地分隔了所有层?您将在这个示例代码上编写什么样的测试?

这取决于您的SharedReferencesLiveData的功能

如果SharedPreferencesLiveData包含特定于Android的类,您将无法正确测试它,因为JUnit将无法访问特定于Android的类

另一个问题是,为了能够观察LiveData,您需要某种生命周期所有者。(原始邮政编码中包含此信息。)

在单元测试中,“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立即执行,而不是在需要时执行

我是否正确地分隔了所有层

这些层似乎合理地分开了。逻辑在ViewModel中,您可以使用它

您会在这个示例代码上编写什么样的测试

测试ViewModel时,您可以在此代码上编写指令插入或纯单元测试。对于单元测试,您可能需要弄清楚如何对首选项进行双重测试,以便关注isFirstLaunch/map行为。一个简单的方法是在ViewModel中通过一个伪偏好测试

我如何测试它

我写了一篇关于测试LiveData转换的简介,请继续阅读

测试LiveData转换 Tl;DR您可以测试LiveData转换,只需确保观察到转换的结果

事实1:LiveData在未观察到数据的情况下不会发出数据。LiveData的“”旨在避免额外的工作。LiveData知道其观察者(通常是活动/片段)所处的生命周期状态。这使LiveData能够知道屏幕上是否有任何东西在观察它。如果未观察到LiveData或其观察者在屏幕外,则不会触发观察者(不会调用观察者的onChanged方法)。这是很有用的,因为它可以防止您进行额外的工作,例如“更新/显示”屏幕外的片段

事实2:必须观察转换生成的LiveData才能触发转换。要触发转换,必须观察结果LiveData(在本例中为isFirstLaunch)。同样,如果没有观察,LiveData观察者不会被触发,转换也不会被触发

当您对ViewModel进行单元测试时,您不应该或不需要访问片段/活动。如果不能以正常的方式设置观察者,如何进行单元测试

事实3:在您的测试中,您不需要LifecycleOwner来观察LiveData,您可以使用ObserveForver您不需要lifecycle Owner来测试LiveData。这是令人困惑的,因为通常在测试之外(即在生产代码中),您将使用类似于活动或片段的

在测试中,您可以使用LiveData方法在没有生命周期所有者的情况下观察。这个观察者“总是”观察,没有屏幕上/屏幕下的概念,因为没有LifecycleOwner。因此,必须使用removeObserver(观察者)手动删除观察者

综上所述,您可以使用ObserveForRever测试转换代码:

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)
        }
    }

}
这将使测试看起来像:

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)

        }
    }

}
class ViewModelTest {

    @get:Rule
    val rule = InstantTaskExecutorRule()

    @Test
    fun isFirstLaunchTest() {

        viewModel.someMethodThatAffectsFirstLaunchLiveData()

        // getOrAwaitValue using the OUTPUT livedata        
        assertEquals(viewModel.isFirstLaunch.getOrAwaitValue(), 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
}

这些选项均取自。

您是否找到了测试转换的正确方法?如果每次测试后创建新的ViewModel,为什么需要从observe forever中删除观察者?收集viewmodel后,垃圾收集器不应该收集观察者吗?
class ViewModelTest {

    @get:Rule
    val rule = InstantTaskExecutorRule()

    @Test
    fun isFirstLaunchTest() {

        viewModel.someMethodThatAffectsFirstLaunchLiveData()

        // getOrAwaitValue using the OUTPUT livedata        
        assertEquals(viewModel.isFirstLaunch.getOrAwaitValue(), true)

    }
}