测试LiveData转换?

时间:2018-08-12 15:39:23

标签: android android-testing android-architecture-components android-livedata android-jetpack

我已经使用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是否正确更新。我该如何测试?我是否正确分离了所有图层?您将在此示例代码上进行什么样的测试?

2 个答案:

答案 0 :(得分:2)

  

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

这些层似乎合理地分开了。逻辑在ViewModel中,您not referring to storing Android Views/Fragments/Activities in the ViewModel

  

您将在此示例代码上进行什么样的测试?

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

  

我该如何测试?

我在测试LiveData转换时写了一些内容,继续阅读!

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

}

一些注意事项:

  • 您需要使用InstantTaskExecutorRule()来获取LiveData更新以同步执行。您需要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)

    }
}

这些选项均来自reactive branch of Architecture Blueprints

答案 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可以立即执行,而不是在需要时执行。