【问题标题】:ViewModel Unit testing multiple view states with LiveData, Coroutines and MockK使用 LiveData、协程和 MockK 测试多个视图状态的 ViewModel 单元
【发布时间】:2020-11-30 00:07:48
【问题描述】:

我在 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
    }
}

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

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

但是我找不到测试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()
}

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

【问题讨论】:

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


    【解决方案1】:

    使用 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
        
    

    【讨论】:

      【解决方案2】:

      我假设您正在使用 mockk

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

         val observer = mockk<Observer<ViewState<YourObject>>> { every { onChanged(any()) } just Runs }
        
      2. 使用之前的观察者对象观察你的实时数据

        viewModel.postStateWithSuspend.observeForever(observer)
        
      3. 调用您的 getPostWithSuspend() 函数

        viewModel.getPostWithSuspend()
        
      4. 验证

         verifySequence {
                observer.onChanged(yourExpectedValue1)
                observer.onChanged(yourExpectedValue2)
            }
        

      【讨论】:

      • 这应该被标记为有效答案!谢谢
      • 这看起来是更好的解决方案,是的,但至少我对verifySequence 块中的检查有问题。它们需要与 LiveData 中发布的实例完全相同。有人知道如何解决这个问题或我缺少什么吗?
      • @PeterG 我遇到了同样的问题并坚持接受的答案。讽刺!这个解决方案对我来说很好,因为我只是在验证状态,每个都是一个 object 所以它总是相同的实例,直到我需要传递数据并测试这个状态 class 就我而言。
      【解决方案3】:
      • 观察 [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
      }
      

      【讨论】:

        【解决方案4】:

        我为 LiveData 编写了自己的 RxJava 风格的测试观察器

        class LiveDataTestObserver<T> constructor(
            private val liveData: LiveData<T>
        ) : Observer<T> {
        
            init {
                liveData.observeForever(this)
            }
        
            private val testValues = mutableListOf<T>()
        
            override fun onChanged(t: T) {
                if (t != null) testValues.add(t)
            }
        
            fun assertNoValues(): LiveDataTestObserver<T> {
                if (testValues.isNotEmpty()) throw AssertionError(
                    "Assertion error with actual size ${testValues.size}"
                )
                return this
            }
        
            fun assertValueCount(count: Int): LiveDataTestObserver<T> {
                if (count < 0) throw AssertionError(
                    "Assertion error! value count cannot be smaller than zero"
                )
                if (count != testValues.size) throw AssertionError(
                    "Assertion error! with expected $count while actual ${testValues.size}"
                )
                return this
            }
        
            fun assertValue(vararg predicates: T): LiveDataTestObserver<T> {
                if (!testValues.containsAll(predicates.asList())) throw AssertionError("Assertion error!")
                return this
            }
        
            fun assertValue(predicate: (List<T>) -> Boolean): LiveDataTestObserver<T> {
                predicate(testValues)
                return this
            }
        
            fun values(predicate: (List<T>) -> Unit): LiveDataTestObserver<T> {
                predicate(testValues)
                return this
            }
        
            fun values(): List<T> {
                return testValues
            }
        
            /**
             * Removes this observer from the [LiveData] which was observing
             */
            fun dispose() {
                liveData.removeObserver(this)
            }
        
            /**
             * Clears data available in this observer and removes this observer from the [LiveData] which was observing
             */
            fun clear() {
                testValues.clear()
                dispose()
            }
        }
        
        fun <T> LiveData<T>.test(): LiveDataTestObserver<T> {
        
            val testObserver = LiveDataTestObserver(this)
        
            // Remove this testObserver that is added in init block of TestObserver, and clears previous data
            testObserver.clear()
            observeForever(testObserver)
        
            return testObserver
        }
        

        并将其用作

           val testObserver = viewModel. postStateWithSuspend.test()
        
                // WHEN
                viewModel. getPostWithSuspend()
        
                // THEN
                testObserver
                    .assertValue { states ->
                        (
                            states[0].status == Status.LOADING &&
                                states[1].status == Status.ERROR
                            )
                    }
        

        【讨论】:

          【解决方案5】:

          您好,您可以通过稍微修改扩展名来解决这个问题。 就像:

          要点是您最初需要增加闩锁计数,然后在闩锁倒数为零之前不要移除观察者。

          剩下的就很简单了,你只需要将结果存储在一个列表中。

          祝你好运!

           /*
           Add for multiple values in LiveData<T>
           */
          @VisibleForTesting(otherwise = VisibleForTesting.NONE)
          fun <T> LiveData<T>.getOrAwaitValuesTest(
              time: Long = 2,
              timeUnit: TimeUnit = TimeUnit.SECONDS,
              maxCountDown: Int = 1,
              afterObserve: () -> Unit = {}
          ): List<T?> {
              val data: MutableList<T?> = mutableListOf()
              val latch = CountDownLatch(maxCountDown)
              val observer = object : Observer<T> {
                  override fun onChanged(o: T?) {
                      data.add(o)
                      latch.countDown()
                      if (latch.count == 0L) {
                          this@getOrAwaitValuesTest.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)
              }
          
              return data.toList()
          }
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2023-03-30
            • 2020-04-12
            • 1970-01-01
            • 2020-05-07
            • 2019-12-17
            • 2021-08-09
            • 1970-01-01
            • 2020-10-17
            相关资源
            最近更新 更多