Testing ViewModel LiveData

Aman Bansal
Mar 26 · 5 min read

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.

class HomeViewModelTest {

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

@Before
fun setupMyTripViewModel() {
homeRepo = mock(HomeRepository::class.java)
viewModel = HomeViewModel(homeRepo)
RxJavaPlugins.setIoSchedulerHandler{Schedulers.trampoline()}
}
}

We don’t need real HomeRepository instance so we will mock HomeRepository class and create HomeViewModel instance using mocked object.

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)))
observer = Observer {
Assert.assertEquals(Status.LOADING,it.status)
}

viewModel.homeItem.observeForever(observer)
viewModel.loadHomeItem()
}

This test is checking that when we call the loadHomeItem() method, our livedata is updated with a change event with Status.LOADING.

We created Observer instance and started observing

viewModel.homeItem.observeForever(observer)

If our livedata doesn’t have an observer, then onChanged events will not be emitted. This observer instance allows us to add a observer to our livedata so that we can verify the data is changed when we expect it to.

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(){

// countdown latch of 3
val
latch = CountDownLatch(3)

//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))

//setup observer
observer = Observer {

when (latch.count) {
3L -> {
Assert.assertEquals(Status.LOADING, it.status)
latch.countDown()
}
2L -> {
Assert.assertEquals(Status.SUCCESS, it.status)
Assert.assertEquals(cacheList, it.data)
latch.countDown()
}
1L -> {
Assert.assertEquals(Status.SUCCESS, it.status)
Assert.assertEquals(apiList, it.data)
latch.countDown()
}
}

}

viewModel.homeItem.observeForever(observer)
viewModel.loadHomeItem()

if(!latch.await(5,TimeUnit.SECONDS)){
throw Throwable("Latch didn't complete, count=$latch.count")
}
}

CountDownLatch is used to make sure that a task waits for other threads before it starts.

Here we’re using CountDownLatch of count 3. Countdown latch will wait for 5 sec and then check if the latch count is 0 then successful else it will throw error.

Observing Emitted Data:

On every emission of data we will assert the value and decrement the count.

It will throw an error if total triggered events is less than 3.

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() {

// countdown latch of 2
val
latch = CountDownLatch(2)

//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")))

//setup observer
observer = Observer {

when (latch.count) {
2L -> {
//assert status
Assert.assertEquals(Status.LOADING, it.status)
latch.countDown()
}

1L -> {
//assert status
Assert.assertEquals(Status.ERROR, it.status)
//assert error message
...

latch.countDown()
}
}
}

viewModel.homeItem.observeForever(observer)
viewModel.loadHomeItem()

if (!latch.await(5, TimeUnit.SECONDS)) {
throw Throwable("Latch didn't complete, count=$latch.count")
}
}

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"

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:

More From Medium

Related reads

Also tagged Programming

Also tagged Android App Development

Also tagged Android App Development

Merge adapters sequentially with MergeAdapter

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade