[英]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库
First you need to create observer object首先你需要创建观察者 object
val observer = mockk<Observer<ViewState<YourObject>>> { every { onChanged(any()) } just Runs }
Observe your livedata using previous observer object使用之前的观察者 object 观察您的实时数据
viewModel.postStateWithSuspend.observeForever(observer)
Call your getPostWithSuspend() function调用你的 getPostWithSuspend() function
viewModel.getPostWithSuspend()
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()
}
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.