测试LiveData转换?

qba*_*ait 13 android android-testing android-livedata android-architecture-components android-jetpack

我使用Android架构组件和反应式方法构建了一个启动画面.我从Preferences LiveData对象返回fun isFirstLaunchLD(): SharedPreferencesLiveData<Boolean>.我有ViewModel将LiveData传递给视图并更新首选项

val isFirstLaunch = Transformations.map(preferences.isFirstLaunchLD()) { isFirstLaunch ->
    if (isFirstLaunch) {
        preferences.isFirstLaunch = false
    }
    isFirstLaunch
}
Run Code Online (Sandbox Code Playgroud)

在我的片段中,我从ViewModel观察LiveData

    viewModel.isFirstLaunch.observe(this, Observer { isFirstLaunch ->
        if (isFirstLaunch) {
            animationView.playAnimation()
        } else {
            navigateNext()
        }
    })
Run Code Online (Sandbox Code Playgroud)

我想现在测试我的ViewModel,看看是否正确更新了isFirstLaunch.我该怎么测试呢?我是否正确分离了所有图层?你会对这个示例代码进行什么样的测试?

Lyl*_*yla 21

我是否正确分离了所有图层?

这些层似乎合理地分开。逻辑在 ViewModel 中,您不是指在 ViewModel中存储 Android Views/Fragments/Activities

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

在测试您的 ViewModel 时,您可以在此代码上编写检测或纯单元测试。对于单元测试,您可能需要弄清楚如何为首选项制作双重测试,以便您可以专注于 isFirstLaunch/map 行为。一个简单的方法是将一个虚假的偏好测试传递给 ViewModel。

我该如何测试?

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

测试 LiveData 转换

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

事实 1:如果没有观察到,LiveData 不会发出数据。LiveData 的“生命周期意识”就是为了避免额外的工作。LiveData 知道它的观察者(通常是活动/片段)所处的生命周期状态。这允许 LiveData 知道它是否正在被屏幕上的任何东西观察到。如果没有观察到 LiveData 或者它们的观察者在屏幕外,则不会触发观察者(不调用观察者的 onChanged 方法)。例如,这很有用,因为它可以防止您做额外的工作“更新/显示”屏幕外的 Fragment。

事实 2:必须观察转换生成的 LiveData 才能触发转换。要触发转换,必须观察结果 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)
        }
    }

}
Run Code Online (Sandbox Code Playgroud)

一些注意事项:

  • 您需要使用InstantTaskExecutorRule()来同步执行 LiveData 更新。您需要androidx.arch.core:core-testing:<current-version>使用此规则。
  • 虽然您经常会observeForever在测试代​​码中看到它,但它有时也会进入生产代码。请记住,当您observeForever在生产代码中使用时,您将失去生命周期感知的好处。您还必须确保不要忘记移除观察者!

最后,如果您要编写大量这样的测试,try、observe-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)
    }
}
Run Code Online (Sandbox Code Playgroud)

这将使测试看起来像:

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)

        }
    }

}
Run Code Online (Sandbox Code Playgroud)

选项 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
}
Run Code Online (Sandbox Code Playgroud)

这将使测试看起来像:

class ViewModelTest {

    @get:Rule
    val rule = InstantTaskExecutorRule()

    @Test
    fun isFirstLaunchTest() {

        viewModel.someMethodThatAffectsFirstLaunchLiveData()

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

    }
}
Run Code Online (Sandbox Code Playgroud)

这些选项均来自Architecture Blueprints反应式分支


Bam*_*Bam 1

这取决于您的SharedPreferencesLiveData的作用。

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

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

在单元测试中,“ this ”可以简单地替换为如下内容:

private fun lifecycle(): Lifecycle {
    val lifecycle = LifecycleRegistry(Mockito.mock(LifecycleOwner::class.java))
    lifecycle.handleLifecycleEvent(Lifecycle.Event.ON_RESUME)
    return lifecycle
}
Run Code Online (Sandbox Code Playgroud)

然后按以下方式使用:

@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
    }
}
Run Code Online (Sandbox Code Playgroud)

注意:您必须存在规则,以便 LiveData 立即执行,而不是随时执行。