Unit testing, a testing technique that uses individual modules to determine if there are any problems by the developer himself. It is concerned with the functional correctness of standalone modules.
So why are writing tests so important?
Our tests actually protect the implementation, and any future changes that break the code will also break the tests. In other words, we will be notified if a change breaks the code.
Good architecture separates concerns into components and makes Unit testing easier.
In this tutorial, I will explain How to write unit tests for ViewModel. This article will implement MVVM and LiveData.
Model-View-ViewModel
- View — which informs the ViewModel about user actions
- ViewModel — exposes the data flow relevant to the View
- The Model is where the business logic resides. It also has no knowledge of the ViewModel but instead sends observable notifications when it is updated.
View and ViewModel communicate via LiveData
The important part here is the View (Activity/Fragments), keeping all the logic in the ViewModel, also dispatching user actions immediately to the ViewModel using LiveData. We should avoid all Android related dependencies in the ViewModel in order to be able to test with pure JUnit tests.
Let's get into the code:
I've used Dagger to provide dependencies, RxJava for API calls and threading, LiveData to notify the view.
Please familiarize yourself with the basics of JUnit Testing
@Before, @After, @Test, @Mock, @RunWith, annotations
https://www.vogella.com/tutorials/JUnit/article.html
Before reading further, I highly recommend you to take a look at the source code of the application to understand how it works. You can find the entire code link on Github ( https://github.com/droiddevgeeks/NewsApp )
ViewModel class
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
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
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
@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
This will tell JUnit to force tests to execute synchronously, especially when using Architecture Components. How can we verify that our ViewModels are triggering the right events for our Views?
Verifying the Observer onChanged() event
We can test our ViewModels by using mockito to verify that our onChanged() observer is called when the postValue() method should be triggered in our ViewModel.
I am using mockito to mock dependencies. If you are facing any issue like mockito not able to spy final class then use it testImplementation org.mockito:mockito-inline:2.13.0
in build.gradle.
there is 1 more way if you use org.mockito:mockito-core
then inside the test folder create a folder "resources"
then inside the resources folder create a new folder "mockito-extensions"
then inside this folder create a file "org.mockito.plugins.MockMaker" with the content "mock-maker-inline"