Unit Testing for ViewModel

Kishan Maurya
MindOrks
4 min readMay 20, 2019

--

Unit testing, a testing technique using which individual modules are tested to determine if there are any issues by the developer himself. It is concerned with the functional correctness of the standalone modules

So why is writing tests so important?
Our test is actually protecting the implementation, and any future changes that break the code will also break the tests. In other words, we will be notified if there are changes that break the code.

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

In this tutorial, I am going to explain How to write a unit test for ViewModel.
The article will implement MVVM and LiveData.

Model-View-ViewModel

  • The View — that informs the ViewModel about the user’s actions
  • The ViewModel — exposes streams of data relevant to the View
  • The Model is where the business logic resides. It also has no knowledge about the ViewModel but rather sends out observable notifications when updated.

View and ViewModel communicates via LiveData

Observer Pattern for updating Activity or Fragment to handle commonly experienced problems.

The important part here is View ( Activity/Fragments), keeping all the logic in ViewModel, also dispatching user actions immediately to ViewModel using LiveData. We should avoid all android related dependencies in ViewModel to be able to test with pure JUnit test.

Let's get into code:
I have used Dagger to provide dependencies, RxJava for API call and threading, LiveData to notify view.

Please make yourself familiar with the basics of JUnit Testing
@Before, @After,@Test, @Mock,@RunWith, annotation
https://www.vogella.com/tutorials/JUnit/article.html

Before continue reading, I highly recommend you to take a look at the app source code to understand how it works. You can find entire code link on Github ( https://github.com/droiddevgeeks/NewsApp )

ViewModel class
https://github.com/droiddevgeeks/NewsApp/blob/master/app/src/main/java/news/agoda/com/sample/ui/viewmodel/NewsViewModel.java

public class NewsViewModel extends ViewModel {

private CompositeDisposable disposable;
private final NewsApiClient apiClient;
private final RxSingleSchedulers rxSingleSchedulers;
private final MutableLiveData<NewsListViewState> newsListState = new MutableLiveData<>();

public MutableLiveData<NewsListViewState> getNewsListState() {
return newsListState;
}

@Inject
public NewsViewModel(NewsApiClient apiClient, RxSingleSchedulers rxSingleSchedulers) {
this.apiClient = apiClient;
this.rxSingleSchedulers = rxSingleSchedulers;
disposable = new CompositeDisposable();
}

public void fetchNews() {
disposable.add(apiClient.fetchNews()
.doOnEvent((newsList, throwable) -> onLoading())
.compose(rxSingleSchedulers.applySchedulers())
.subscribe(this::onSuccess,
this::onError));
}

private void onSuccess(NewsList newsList) {
NewsListViewState.SUCCESS_STATE.setData(newsList);
newsListState.postValue(NewsListViewState.SUCCESS_STATE);
}

private void onError(Throwable error) {
NewsListViewState.ERROR_STATE.setError(error);
newsListState.postValue(NewsListViewState.ERROR_STATE);
}

private void onLoading() {
newsListState.postValue(NewsListViewState.LOADING_STATE);
}

@Override
protected void onCleared() {
super.onCleared();
if (disposable != null) {
disposable.clear();
disposable = null;
}
}
}

NewsViewState class
https://github.com/droiddevgeeks/NewsApp/blob/master/app/src/main/java/news/agoda/com/sample/ui/viewmodel/NewsListViewState.java

public class NewsListViewState extends BaseViewState<NewsList> {
private NewsListViewState(NewsList data, int currentState, Throwable error) {
this.data = data;
this.error = error;
this.currentState = currentState;
}

public static NewsListViewState ERROR_STATE = new NewsListViewState(null, State.FAILED.value, new Throwable());
public static NewsListViewState LOADING_STATE = new NewsListViewState(null, State.LOADING.value, null);
public static NewsListViewState SUCCESS_STATE = new NewsListViewState(new NewsList(), State.SUCCESS.value, null);

}

BaseView State
https://github.com/droiddevgeeks/NewsApp/blob/master/app/src/main/java/news/agoda/com/sample/base/BaseViewState.java

public class BaseViewState<T> {
public T getData() {
return data;
}

public void setData(T data) {
this.data = data;
}

public Throwable getError() {
return error;
}

public void setError(Throwable error) {
this.error = error;
}

public int getCurrentState() {
return currentState;
}

public void setCurrentState(int currentState) {
this.currentState = currentState;
}

protected T data;
protected Throwable error;
protected int currentState;

public enum State{
LOADING(0), SUCCESS(1),FAILED(-1);
public int value;
State(int val) {
value = val;
}
}
}

ViewModelTest class
https://github.com/droiddevgeeks/NewsApp/blob/master/app/src/test/java/news/agoda/com/sample/ui/viewmodel/NewsViewModelTest.java

@RunWith(JUnit4.class)
public class NewsViewModelTest {
@Rule
public InstantTaskExecutorRule instantExecutorRule = new InstantTaskExecutorRule();

@Mock
ApiEndPoint apiEndPoint;
@Mock
NewsApiClient apiClient;
private NewsViewModel viewModel;
@Mock
Observer<NewsListViewState> observer;


@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
viewModel = new NewsViewModel(apiClient, RxSingleSchedulers.TEST_SCHEDULER);
viewModel.getNewsListState().observeForever(observer);
}

@Test
public void testNull() {
when(apiClient.fetchNews()).thenReturn(null);
assertNotNull(viewModel.getNewsListState());
assertTrue(viewModel.getNewsListState().hasObservers());
}

@Test
public void testApiFetchDataSuccess() {
// Mock API response
when
(apiClient.fetchNews()).thenReturn(Single.just(new NewsList()));
viewModel.fetchNews();
verify(observer).onChanged(NewsListViewState.LOADING_STATE);
verify(observer).onChanged(NewsListViewState.SUCCESS_STATE);
}

@Test
public void testApiFetchDataError() {
when(apiClient.fetchNews()).thenReturn(Single.error(new Throwable("Api error")));
viewModel.fetchNews();
verify(observer).onChanged(NewsListViewState.LOADING_STATE);
verify(observer).onChanged(NewsListViewState.ERROR_STATE);
}

@After
public void tearDown() throws Exception {
apiClient = null;
viewModel = null;
}

InstantTaskExecutorRule
It will tell JUnit to force tests to be executed synchronously, especially when using Architecture Components.

How can we verify our ViewModels are triggering the right events for our Views?

Verifying Observer onChanged() events

We can test our ViewModels is by using mockito to verify that our observer onChanged() is called when a postValue() method should have been triggered within our ViewModel.

I am using mockito to mock dependencies. If you face problem like mockito cant spy final class then use
testImplementation 'org.mockito:mockito-inline:2.13.0'
in the build.gradle

there is 1 more way if you are using org.mockito:mockito-core
then inside test folder create folder “resources
then inside resources folder create new folder “mockito-extensions
then inside this folder create file “org.mockito.plugins.MockMaker” with content “mock-maker-inline

Here is a link for the next part for ViewModel testing.
https://medium.com/mindorks/unit-testing-viewmodel-part-2-4a1fa93d656d

https://medium.com/mindorks/effective-livedata-testing-13d17b555d9b

Soon I will post more articles on android, core java.
till then happy learning… :)

--

--