Kotlin Unit Testing ViewModel (KUTVM)

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

  1. View — which informs the ViewModel about user actions
  2. ViewModel — exposes the data flow relevant to the View
  3. 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"


Post a Comment

Previous Next

نموذج الاتصال