![](/img/trans.png)
[英]ViewModel unit testing with Repository + LiveData + Coroutines
[英]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庫
首先你需要創建觀察者 object
val observer = mockk<Observer<ViewState<YourObject>>> { every { onChanged(any()) } just Runs }
使用之前的觀察者 object 觀察您的實時數據
viewModel.postStateWithSuspend.observeForever(observer)
調用你的 getPostWithSuspend() function
viewModel.getPostWithSuspend()
驗證它
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()
}
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.