Testing ViewModels and Activities in Your MVVM App

TribalScale Inc.
TribalScale
Published in
6 min readJan 30, 2019

--

By Yawei Li & Shaurya Arora

This is part 3 of our 3-part series on implementing MVVM (Model-View-ViewModel) on Android using Google’s ViewModel and LiveData architecture components. In this part, we will take a look at how to write unit tests for an app that uses ViewModel and LiveData.

Read from the beginning if you’re not familiar with MVVM or Google’s architecture components.

Testing the view model

One of the main goals of MVVM is to minimize the complexity of activities and fragments. As a result, most of the logic lives inside the view model. Therefore, it’s especially important to test our view model thoroughly. Here’s a quick recap of our ViewModel class:

We have 3 LiveDatas exposed to the activity: teams, shouldShowError and isLoading. isLoading() is initialized to true and changed to false at the end of loadTeams(). This happens regardless of repository.refreshTeams() successfully fetching data from the network, or throwing an exception. In both situations, we set isLoading’s value to false so that our observing activity can hide the loading animation. Data is fetched from the network and stored locally in the database in a separate DataRepository class. Moreover, we only want to fetch data from the network if it is not already locally present.

Setting up the test

We run our test class with RobolectricTestRunner. This is because ViewModel depends on classes within the Android SDK. We create the necessary variables, as well as mocks for dependencies. Note that, since TeamViewModel actually performs a transformation on repository.teams, we need to make teamListLiveData a spy. If it were just a mock, the transformation wouldn’t be triggered even though viewModel.teams is being observed.

Test 1: isLoading should be false after calling repo.refreshTeams()

In the first test case we can verify that isLoading’s value starts out as true, and that after calling loadTeams(), the value gets changed to false. We can also verify that calling loadTeams() triggers refreshTeams() inside the repository. The test is wrapped inside runBlocking {} to ensure that all running coroutines get a chance to complete before the test function returns.

Test 2: isError should be true if repo.refreshTeams() throws some exception

For this case, we want to verify that isError is initially set to false, and that it gets changed to true after loadTeams() is called if repo.refreshTeams() threw an exception.

Note: Kotlin allows us to write our test function names as sentences by enclosing them in back-ticks (`). This improves readability in general, and especially when running your test suite.

Test 3: repo.refreshTeams() should be called when no data in the database

In this test case, if data returned from the database is empty, we want to verify that repo.refreshTeams() is called — that is, data is fetched in the network.

At the beginning of the test, we simulate the condition of “no data in the database” by setting teamListLiveData’s value as an empty list. We start observing viewModel.teams via the observeForever() method, which simulates an observer in an active state. This ensures that an update in the value of the LiveData object backing viewModel.teams is prepared to be delivered, hence triggering the map transformation inside the view model. At the end, we also assert that isLoading and isError have the expected values.

Test 4: repo.refreshTeams() should not be called if data already in the database

In this final test, we want to verify that repo.refreshTeams() is never called if the list of teams is already present in the local database.

The structure of this test is quite similar to that of the last test. However, now, we set the value of teamListLiveData to an actual list of teams to simulate data that is already stored in the database. Moreover, we verify that repo.refreshTeams() is never called using the never() invocation.

Testing the activity

Let’s take a quick look at our Activity class before we begin writing tests:

As we can see, the majority of the data and logic is outside the activity. MainActivity is simply observing the data points exposed by TeamViewModel and updating the UI based on updates in those data points. Therefore, to test the activity, we will verify that our UI is in the correct state when LiveDatas in the view model deliver updates to MainActivity via the onChanged() method of the Observers. Note, also, that we have a @TestOnly setter for the viewModel property so that we can inject a mock view model in tests (can be avoided by using a DI framework such as Koin). We moved the subscribeUi() call from onCreate() to onStart() so that in our tests, we have a chance to inject the mock ViewModel object before the Observers are attached.

Setting up the test

We create variables for an instance of MainActivity, as well as for an instance of ActivityController<MainActivity> which will enable us to drive the MainActivity instance through different lifecycle stages. We create mocks for TeamViewModel, as well as for each of the three LiveDatas exposed by it (teams, isLoading and isError). Finally, we create ArgumentCaptors for each of the three Observers in MainActivity, and add in our MockitoRule to initialize all annotated elements.

In the setUp() function, we first invoke onCreate() of the activity (via activityController.create()). Then we swap out the real view model inside the activity with a mock via the setTestViewModel() function. Only then do we start the activity via activityController.start().

Calling activityController.start() invokes MainActivity’s onStart() function, which in turn invokes the subscribeUi() function where all the observe calls take place. We use Mockito’s verify() function on each of the LiveData mocks together with our captors to get the Observers that are attached inside subscribeUi().

Tests 1 and 2: initial view states

To start, we can write 2 quick and simple tests to assert that initially, the loading spinner is visible, and the error view and teams RecyclerView are not shown.

Test 3: list of teams should be displayed when successfully retrieved

In this test, we simulate the success case: a valid list of teams is sent through by the view model.

As noted in the setup stage, we use our captors to simulate different test cases. Here, in the success case, we call onChanged() on the Observer for teams with a valid list containing two Team objects. Moreover, we call onChanged() on the Observers for isLoading and isError with false. Then, we assert that the teams RecyclerView is visible, and the loading spinner and error view are not shown. We also assert that the adapter of that RecyclerView is holding on to a list of teams which is identical to the one that we passed in.

Test 4: error message should be shown if ViewModel encounters exception

Finally, we test the error case: an error message should be displayed if the ViewModel signals an error in retrieving the list of teams.

In this case, we don’t really care whether the Observer for teams is invoked or not. We mostly care that the Observer for isError is invoked with true, and that the Observer for isLoading is invoked with false. We can see in `TeamViewModel` that this is the exact error scenario. Now, we assert that only the error message is visible, and that all other UI elements are not shown.

And that’s how you can test ViewModels and related activities!

Yawei Li is a Toronto-based Android developer and an avid dog lover.

Shaurya Arora is a Toronto-based Android developer, drummer and prog metal/djent lover.

Join our fast growing team and connect with us on Twitter, LinkedIn, Instagram& Facebook! Learn more about us on our website.

--

--

TribalScale Inc.
TribalScale

A digital innovation firm with a mission to right the future.