Reputation: 66759
I have a function in ViewModel with 2 states, first state is always LOADING, second state depends on result of api or db interactions.
This is the 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
@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
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()
[email protected](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. Is there a way to test multiple values of LiveData
?
Upvotes: 12
Views: 9525
Reputation: 199
alternative you can use 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)
}
Upvotes: 1
Reputation: 87
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.
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) {
[email protected](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()
}
Upvotes: 2
Reputation: 2529
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
}
Upvotes: 6
Reputation: 310
I assume you are using mockk library
First you need to create observer object
val observer = mockk<Observer<ViewState<YourObject>>> { every { onChanged(any()) } just Runs }
Observe your livedata using previous observer object
viewModel.postStateWithSuspend.observeForever(observer)
Call your getPostWithSuspend() function
viewModel.getPostWithSuspend()
Verify it
verifySequence {
observer.onChanged(yourExpectedValue1)
observer.onChanged(yourExpectedValue2)
}
Upvotes: 17
Reputation: 66759
I wrote my own RxJava style test observer for LiveData
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
)
}
Upvotes: 2
Reputation: 2742
Using mockk you can capture the values and store it in the list, then you check the values by order.
//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
Upvotes: 19