简体   繁体   English

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

[英]ViewModel Unit testing multiple view states with LiveData, Coroutines and MockK

I have a function in ViewModel with 2 states, first state is always LOADING, second state depends on result of api or db interactions.我在 ViewModel 中有一个 function 有两个状态,第一个 state 总是在加载,第二个 state 取决于 api 或数据库交互的结果。

This is the function这是function

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

And no error, test works fine for checking final result of ERROR or SUCCESS没有错误,测试可以很好地检查 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() }
        }

But i couldn't find a way to test whether LOADING state has occurred or not, so i modified existing extension function to但是我找不到一种方法来测试LOADING state 是否已经发生,所以我将现有的扩展 function 修改为

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

To add data to a list when LiveData changes and store states in that list but it never returns LOADING state because it happens before observe starts.在 LiveData 更改时将数据添加到列表并将状态存储在该列表中,但它永远不会返回 LOADING state,因为它发生在观察开始之前。 Is there a way to test multiple values of LiveData ?有没有办法测试LiveData的多个值?

Using mockk you can capture the values and store it in the list, then you check the values by order.使用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
    

I assume you are using mockk library我假设您正在使用mockk

  1. First you need to create observer object首先你需要创建观察者 object

     val observer = mockk<Observer<ViewState<YourObject>>> { every { onChanged(any()) } just Runs }
  2. Observe your livedata using previous observer object使用之前的观察者 object 观察您的实时数据

    viewModel.postStateWithSuspend.observeForever(observer)
  3. Call your getPostWithSuspend() function调用你的 getPostWithSuspend() function

     viewModel.getPostWithSuspend()
  4. Verify it验证它

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

Hi you may simply solve this problem by modifying the extension a little bit.您好,您可以通过稍微修改扩展名来简单地解决这个问题。 It's like:就像是:

The main points are you need to increase the latch count initially then don't remove the observer until the latch is count down to zero.要点是您最初需要增加锁存器计数,然后在锁存器倒计时到零之前不要移除观察器。

The rest would be easy, you just need to store your result in a list. rest 很简单,您只需要将结果存储在列表中即可。

Good luck!祝你好运!

 /*
 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()
}
  • Observes [LiveData] and captures latest value and all subsequent values, returning them in ordered list.观察 [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
}

I wrote my own RxJava style test observer for LiveData我为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
}

And use it as并将其用作

   val testObserver = viewModel. postStateWithSuspend.test()

        // WHEN
        viewModel. getPostWithSuspend()

        // THEN
        testObserver
            .assertValue { states ->
                (
                    states[0].status == Status.LOADING &&
                        states[1].status == Status.ERROR
                    )
            }

alternative you can use mockito-kotlin或者你可以使用 mockito-kotlin

//mock the observer 
@Mock
private lateinit var observer: Observer<AnyObject>

@Test
fun `Your test`(){

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

  //capture the values
  argumentCaptor<AnyObject>().apply {

  Mockito.verify(observer, Mockito.times(2)).onChanged(capture())
     //assert your values
   Truth.assertThat("Loading").isEqualTo(allValues[0]?.state) 
   Truth.assertThat("Network error occurred.").isEqualTo(allValues[1]?.error?.message)            
   Truth.assertThat(allValues[1]?.error).isInstanceOf(Exception::class.java)
   
    }
 //do not forget to remove the observer
 viewModel.postStateWithSuspend.removeObserver(observer)
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM