Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.

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?

like image 295
Thracian Avatar asked Aug 10 '20 11:08

Thracian


People also ask

How do you write a unit test case for a ViewModel?

In this codelab, you use the JUnit framework to write unit tests. To use the framework, you need to add it as a dependency in your app module's build. gradle file. You can now use this library in your app's source code, and Android studio will help to add it to the generated Application Package File (APK) file.

What is the difference between LiveData and ViewModel?

ViewModel : Provides data to the UI and acts as a communication center between the Repository and the UI. Hides the backend from the UI. ViewModel instances survive device configuration changes. LiveData : A data holder class that follows the observer pattern, which means that it can be observed.

How do you observe LiveData in unit testing?

First, the LiveData should be observed in order to work properly. You can find a utility class at the end of the section to observe your LiveData from test class. Secondly we should add InstantExecutorRule test rule. InstantExecutorRule is a JUnit rule and it comes with androidx.

How do you observe mutable live data in ViewModel?

You have to change the value of the live data instead of just adding an item to the already set list. This will take your existing list then add your new item and then set the value of the live data so that the observe will be called.


2 Answers

  • Observes [LiveData] and captures latest value and all subsequent values, returning them in ordered list.
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
}
like image 54
Jeff Padgett Avatar answered Oct 28 '22 00:10

Jeff Padgett


I assume you are using mockk library

  1. First you need to create observer object

     val observer = mockk<Observer<ViewState<YourObject>>> { every { onChanged(any()) } just Runs }
    
  2. Observe your livedata using previous observer object

    viewModel.postStateWithSuspend.observeForever(observer)
    
  3. Call your getPostWithSuspend() function

    viewModel.getPostWithSuspend()
    
  4. Verify it

     verifySequence {
            observer.onChanged(yourExpectedValue1)
            observer.onChanged(yourExpectedValue2)
        }
    
like image 23
aliftc12 Avatar answered Oct 28 '22 01:10

aliftc12