ViewModel 使用 LiveData、Coroutines 和 MockK 单元测试多个视图状态

Thr*_*ian 9 android unit-testing android-livedata kotlin-coroutines

我在 ViewModel 中有一个具有 2 个状态的函数,第一个状态始终为 LOADING,第二个状态取决于 api 或 db 交互的结果。

这是功能

fun getPostWithSuspend() {

    myCoroutineScope.launch {

        // Set current state to LOADING
        _postStateWithSuspend.value = ViewState(LOADING)

        val result = postsUseCase.getPosts()

        // Check and assign result to UI
        val resultViewState = if (result.status == SUCCESS) {
            ViewState(SUCCESS, data = result.data?.get(0)?.title)
        } else {
            ViewState(ERROR, error = result.error)
        }

        _postStateWithSuspend.value = resultViewState
    }
}
Run Code Online (Sandbox Code Playgroud)

没有错误,测试可以很好地检查 ERROR 或 SUCCESS 的最终结果

   @Test
    fun `Given DataResult Error returned from useCase, should result error`() =
        testCoroutineRule.runBlockingTest {

            // GIVEN
            coEvery {
                useCase.getPosts()
            } returns DataResult.Error(Exception("Network error occurred."))

            // WHEN
            viewModel.getPostWithSuspend()

            // THEN
            val expected = viewModel.postStateWithSuspend.getOrAwaitMultipleValues(dataCount = 2)

//            Truth.assertThat("Network error occurred.").isEqualTo(expected?.error?.message)
//            Truth.assertThat(expected?.error).isInstanceOf(Exception::class.java)
            coVerify(atMost = 1) { useCase.getPosts() }
        }
Run Code Online (Sandbox Code Playgroud)

但是我找不到测试LOADING状态是否发生的方法,所以我将现有的扩展函数修改为

fun <T> LiveData<T>.getOrAwaitMultipleValues(
    time: Long = 2,
    dataCount: Int = 1,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): List<T?> {

    val data = mutableListOf<T?>()
    val latch = CountDownLatch(dataCount)

    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data.add(o)
            latch.countDown()
            this@getOrAwaitMultipleValues.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.toList()
}
Run Code Online (Sandbox Code Playgroud)

在 LiveData 更改时将数据添加到列表并将状态存储在该列表中,但它永远不会返回 LOADING 状态,因为它发生在观察开始之前。有没有办法测试多个值LiveData

iam*_*sal 10

使用mockk,您可以捕获值并将其存储在列表中,然后按顺序检查值。

    //create mockk object
    val observer = mockk<Observer<AnyObject>>()

    //create slot
    val slot = slot<AnyObject>()

    //create list to store values
    val list = arrayListOf<AnyObject>()

    //start observing
    viewModel.postStateWithSuspend.observeForever(observer)


    //capture value on every call
    every { observer.onChanged(capture(slot)) } answers {

        //store captured value
        list.add(slot.captured)
    }

    viewModel.getPostWithSuspend()
    
    //assert your values here
    
Run Code Online (Sandbox Code Playgroud)


ali*_*c12 9

我假设您正在使用mockk

  1. 首先你需要创建观察者对象

     val observer = mockk<Observer<ViewState<YourObject>>> { every { onChanged(any()) } just Runs }
    
    Run Code Online (Sandbox Code Playgroud)
  2. 使用先前的观察者对象观察您的实时数据

    viewModel.postStateWithSuspend.observeForever(observer)
    
    Run Code Online (Sandbox Code Playgroud)
  3. 调用您的 getPostWithSuspend() 函数

    viewModel.getPostWithSuspend()
    
    Run Code Online (Sandbox Code Playgroud)
  4. 验证一下

     verifySequence {
            observer.onChanged(yourExpectedValue1)
            observer.onChanged(yourExpectedValue2)
        }
    
    Run Code Online (Sandbox Code Playgroud)

  • 是的,这看起来是更好的解决方案,但至少我对“verifySequence”块中的检查有问题。它们需要与 LiveData 中发布的实例完全相同。有谁知道如何解决这个问题或我缺少什么? (3认同)
  • 这应该被标记为有效答案!谢谢 (2认同)

Jef*_*ett 6

  • 观察 [LiveData] 并捕获最新值和所有后续值,以有序列表形式返回它们。
inline fun <reified T > LiveData<T>.captureValues(): List<T?> {
    val mockObserver = mockk<Observer<T>>()
    val list = mutableListOf<T?>()
    every { mockObserver.onChanged(captureNullable(list))} just runs
    this.observeForever(mockObserver)
    return list
}
Run Code Online (Sandbox Code Playgroud)