簡體   English   中英

ViewModel 使用 LiveData、Coroutines 和 MockK 對多個視圖狀態進行單元測試

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

我在 ViewModel 中有一個 function 有兩個狀態,第一個 state 總是在加載,第二個 state 取決於 api 或數據庫交互的結果。

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

沒有錯誤,測試可以很好地檢查 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() }
        }

但是我找不到一種方法來測試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()
}

在 LiveData 更改時將數據添加到列表並將狀態存儲在該列表中,但它永遠不會返回 LOADING state,因為它發生在觀察開始之前。 有沒有辦法測試LiveData的多個值?

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

我假設您正在使用mockk

  1. 首先你需要創建觀察者 object

     val observer = mockk<Observer<ViewState<YourObject>>> { every { onChanged(any()) } just Runs }
  2. 使用之前的觀察者 object 觀察您的實時數據

    viewModel.postStateWithSuspend.observeForever(observer)
  3. 調用你的 getPostWithSuspend() function

     viewModel.getPostWithSuspend()
  4. 驗證它

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

您好,您可以通過稍微修改擴展名來簡單地解決這個問題。 就像是:

要點是您最初需要增加鎖存器計數,然后在鎖存器倒計時到零之前不要移除觀察器。

rest 很簡單,您只需要將結果存儲在列表中即可。

祝你好運!

 /*
 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()
}
  • 觀察 [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
}

我為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
                    )
            }

或者你可以使用 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