Testing ViewModel LiveData

Aman Bansal
5 min readMar 26, 2020

Ensure your ViewModel does what it’s supposed to do.

Android Architecture Component was launched in Google IO 2017. One of the key thoughts of Architecture Components is Observer Pattern for updating Activity and Fragment. We use LiveData which resides in ViewModel and observed in Activity/Fragment. It emits the data whenever there is a change and updates the UI.

We use ViewModel to store and manage UI-related data and LiveData to pass the same data to Activity/Fragments. As a result, most of the logic lives inside the view model. Using ViewModel separates the business login from UI-related logic. Therefore, it’s especially important to test our view model thoroughly.

A good architecture separates concerns into components and makes Unit testing become easier.

In this article we will talk about effective way of testing your ViewModel.

For the reference this is how our ViewModel looks like which we are going to test.

I have used Dagger to provide dependencies, RxJava for API call , room DB for cache, threading and LiveData to notify view.

Setting up HomeViewModelTest

First we need to set rule to use Architecture Components

@Rule
val instantExecutorRule = InstantTaskExecutorRule()

InstantTaskExecutorRule comes from the androidx.arch.core:core-testing library.

A JUnit Test Rule that swaps the background executor used by the Architecture Components with a different one which executes each task synchronously.

@RunWith(MockitoJUnitRunner.class)
class
HomeViewModelTest {

private lateinit var viewModel: HomeViewModel
@Mock
private lateinit var homeRepo: HomeRepository
@Mock
private var observer:Observer<Resource<List<HomeItem>>>
@Captor
private lateinit var argumentCaptor: ArgumentCaptor<Resource<List<HomeItem>>>
@get:Rule
public val instantExecutorRule = InstantTaskExecutorRule()

@Before
fun setupMyTripViewModel() {
RxJavaPlugins.setIoSchedulerHandler{Schedulers.trampoline()}
}
}

@Mock creates mock object of given class or interface.

@Captor will create the object of ArgumentCaptor of Resource<List<HomeItem>>. ArgumentCaptor is used to capture argument values for further assertions.

To use @Mock and @Captor annotations, we need to run out test with MockitoJUnitRunner.

RxJavaPlugins.setIoSchedulerHandler{Schedulers.trampoline()}

Schedulers.trampoline() method creates and returns a Scheduler that queues work on the current thread to be executed. So code will run synchronously wherever we use Schedulers.io().

We have utility class HomeItemFatory, through which we will get the mocked list of HomeItem.

@Test
fun loadHomeItemTriggersLoadingState(){
Mockito.`when`(homeRepository.getHomeCache())
.thenReturn(Single.just(HomeItemFatory.getListOf(1)))
Mockito.`when`(homeRepository.getHomeItems())
.thenReturn(Single.just(HomeItemFatory.getListOf(2)))
Mockito.verify(observer, Mockito.times(3))
.onChanged(argumentCaptor.capture())
viewModel.homeItem.observeForever(observer)
viewModel.loadHomeItem()
val values = arugmentCaptor.getAllValues();
Assert.assertEquals(Status.LOADING,values.get(0).status)
}

Mockito.verify verifies certain behavior happened at least once / exact number of times / never.

times(3) allows verifying exact number of invocations.

argumentCaptor.capture() use it to capture the argument.

argumentCaptor.getAllValues() returns all captured values.

Here we are using argumentCaptor to capture arguments values passed in onChanged method of Observer.

Our livedata will fire 3 events, we’ll see later in this post what events it will be.

This test is checking that when we call the loadHomeItem() method, our livedata will fire the first event with Status.LOADING.

We started observing using mocked instance.

viewModel.homeItem.observeForever(observer)

If our livedata doesn’t have an observer, then onChanged events will not be called. This observer instance allows us to add a observer to our livedata so that we can capture arguments get passed into onChange.

values.get(0) will give return the arguments value which was captured on firest invocation.

Another approach of testing livedata is directly get value instead of setting the observer.

val resource = viewModel.homeItem.value
Assert.assertEquals(Status.LOADING,resource.status)

So the question is why do we need observer?

By observing LiveData means that updating the view the whenever it emits the data. So it will trigger event multiple times depending on our code.

How do we test the triggered events ?

This is where Observer comes into the picture when we’re testing LiveData. Event will be triggered everytime it emits.

When everything went good

Now let’s check our ViewModel again, what we are doing is, on calling loadHomeItem()

  1. update the LiveData with LOADING state
  2. then update the LiveData with SUCCESS state and cache data
  3. then after successfully getting the remote data from API, we’re updating the LiveData with SUCCESS state and remote data. And if we get any error then we update the LiveData with ERROR state and message.

With that said we will be verifying 3 values of LiveData.

@Test
fun loadHomeItem_triggers_3_state_success(){

//mocking cache data
val
cacheList =HomeItemFatory.getListOf(1)
Mockito.`when`(homeRepository.getHomeCache())
.thenReturn(Single.just(cacheList))

//mocking api data
val
apiList = HomeItemFatory.getListOf(2)
Mockito.`when`(homeRepository.getHomeItems())
.thenReturn(Single.just(apiList))


Mockito.verify(observer, Mockito.times(3))
.onChanged(argumentCaptor.capture())
viewModel.homeItem.observeForever(observer)
viewModel.loadHomeItem()
val values = arugmentCaptor.getAllValues();
Assert.assertEquals(Status.LOADING,values.get(0).status)

Assert.assertEquals(Status.SUCCESS, values.get(1).status)
Assert.assertEquals(cacheList, values.get(1).data)

Assert.assertEquals(Status.SUCCESS, values.get(2).status)
Assert.assertEquals(cacheList, values.get(2).data)

}

times(3) will verify the 3 invocation else it will throw error. We need to verify the livedata trigger 3 events.

Then we will assert the all 3 eventvalues.

When Something went wrong

Next we will test the scenario when there is no cache data available, that means we are not going to post any value to live data, and another is we got an API error.

Which means we will have 2 triggered events

@Test
fun loadHomeItem_no_cache_api_fail() {

//mocking cache data
val
cacheList = emptyList<HomeItem>()
Mockito.`when`(homeRepository.getHomeCache())
.thenReturn(Single.just(cacheList))

//mocking api data
Mockito.`when`(homeRepository.getHomeItems())
.thenReturn(Single.error(Throwable("Api Error")))
Mockito.verify(observer, Mockito.times(2))
.onChanged(argumentCaptor.capture())

viewModel.homeItem.observeForever(observer)
viewModel.loadHomeItem()
val values = arugmentCaptor.getAllValues();
Assert.assertEquals(Status.LOADING,values.get(0).status)

Assert.assertEquals(Status.ERROR, values.get(1).status)

}

First event will be of LOADING state and second event will be of ERROR state with error message

I am using mockito android to mock dependencies.

"org.mockito:mockito-android:3.2.0"

Edit: I updated this post and using @Mock and @Captor annotations.

That’s it! That’s all we wanted to discuss here regarding ViewModel testing in this blog.

I hope that this blog has given you a understanding of testing your ViewModel and why it is necessary to do.

Thank you so much for your time.

Keep Learning, Keep sharing!

If you like this story, don’t forget to clap 😉

If you want to connect, hit me up on Twitter

Other blogs:

--

--