【问题标题】:how to unit test a function that returns livedata如何对返回 livedata 的函数进行单元测试
【发布时间】:2020-05-31 08:41:20
【问题描述】:

在我的 viewModel 中,我有一个返回 liveData 的函数。该函数在片段中直接调用,因此可以直接在那里观察到。我无法了解如何测试此函数,因为在测试的情况下未观察到该函数发出的 liveData,因此它不会返回该值。

这是我的函数,我想写测试:

    fun saveRating(rating: Float, eventName: String): LiveData<Response<SaveRatingData?>?> {
        val request = RatingRequest(rating.toDouble(), eventName, false)

        return liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
            emit(repository.saveRatings(request))
        }

    }

这就是我在片段中的调用方式:

   viewModel.saveRating(rating, npsEventData?.eventName ?: "").observe(this, Observer {
      // on getting data
   })

提前致谢!

【问题讨论】:

    标签: android unit-testing android-livedata kotlin-coroutines android-viewmodel


    【解决方案1】:

    您需要有一个 testCoroutineDispatcher 或 testCoroutineScope 才能将 viewModel 的范围设置为测试范围。

    class TestCoroutineRule : TestRule {
    
        private val testCoroutineDispatcher = TestCoroutineDispatcher()
    
        val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
    
        override fun apply(base: Statement, description: Description?) = object : Statement() {
    
            @Throws(Throwable::class)
            override fun evaluate() {
    
                Dispatchers.setMain(testCoroutineDispatcher)
    
                base.evaluate()
    
                Dispatchers.resetMain()
                try {
                    testCoroutineScope.cleanupTestCoroutines()
                } catch (exception: Exception) {
                    exception.printStackTrace()
                }
            }
        }
    
        fun runBlockingTest(block: suspend TestCoroutineScope.() -> Unit) =
            testCoroutineScope.runBlockingTest { block() }
    
    }
    

    在任何官方 kotlin 或 Android 文档中都没有提到 Try-catch 块,但是测试异常会导致异常,而不是像我在 here 中所要求的那样通过测试。

    我在 testCoroutineDispatcher 作为调度程序时遇到的另一件事不足以让某些测试通过,您需要将 coroutineScope 而不是调度程序注入 viewModel。

    例如

    fun throwExceptionInAScope(coroutineContext: CoroutineContext) {
    
    
        viewModelScope.launch(coroutineContext) {
    
            delay(2000)
            throw RuntimeException("Exception Occurred")
        }
    }
    

    你有这样一个抛出异常的函数,你将 testCoroutineContext 传递给这个失败的测试。

    @Test(expected = RuntimeException::class)
    fun `Test function that throws exception`() =
        testCoroutineDispatcher.runBlockingTest {
    
            // ? Using testCoroutineDispatcher causes this test to FAIL
            viewModel.throwExceptionInAScope(testCoroutineDispatcher.coroutineContext)
    
            // ? This one passes since we use context of current coroutineScope
            viewModel.throwExceptionInAScope(this.coroutineContext)
    
        }
    

    如果你使用MyViewModel(private val coroutineScope: CoroutineScope)类,它就会通过

    现在,让我们来看看如何使用异步任务测试 liveData。我使用这个类,谷歌的 LiveDataTestUtil 类,用于同步 liveData

    @get:Rule
    var instantTaskExecutorRule = InstantTaskExecutorRule()
    

    按照惯例

    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)
    
        afterObserve.invoke()
    
        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            this.removeObserver(observer)
            throw TimeoutException("LiveData value was never set.")
        }
    
        @Suppress("UNCHECKED_CAST")
        return data as T
    }
    
    /**
     * 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)
        }
    }
    

    现在,您可以像测试同步代码一样对其进行测试

    @Test
    fun `Given repo saves response, it should return the correct one` = testCoroutineScope.runBlockingTest {
    
            // GIVEN
            val repository = mockk<<Repository>()
            val actual = Response(...)
            coEvery { repository.saveRatings } returns actual
    
            // WHEN
            val expected = viewModel.saveResponse()
    
            // THEN
            Truth.assertThat(actual).isEqualTo(expected)
    
        }
    

    我使用了 mockK,它适用于暂停模拟。

    此外,如果您有改造或房间功能调用,则不需要使用Dispatchers.IO,如果您不执行改造或房间操作以外的其他任务,它们会使用自己的线程和挂起修饰符。

    【讨论】:

    • 谢谢@Thracian!它有帮助,但我也必须添加 InstantTaskExecutorRule。没有它,它会在 liveData 上获得 NullPointerException。
    • 不客气,我也在上面写过 LiveDataTestUtils 但它没有设置为代码显示,所以你可能错过了。我现在把它改成代码。
    猜你喜欢
    • 2020-09-28
    • 2021-12-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-25
    • 1970-01-01
    相关资源
    最近更新 更多