Testing Android ViewModels

In my previous post I described how to implement injectable Android view models using Dagger and ViewModel library from Android Architecture Components. In this post I will show a simple way to unit test the view model created then. You can find the full code in the same repository as previously on GitHub.

The structure


The MainViewModel view model exposes three RxJava Observables which Activity (MainActivity) subscribes to in order to receive notifications, e.g. to display an error message. There is also the getRepo function that triggers fetching some data from the GitHub API and the data variable that stores the fetched data.

The mentioned members are divided into three interfaces implemented by the view model but it’s mainly for the clarity of the example (you can easily tell which members are used by the Activity and the Fragments).

The only view model’s dependency (provided by Dagger) is GitHubClient which has a method for fetching some data.

It’s also worth noting that the Activity and the Fragments use the same instance of MainViewModel. That’s why it can both fetch the data when LoadingFragment requests it, tell MainActivity to show an error if anything goes wrong and work as a data store for DataFragment. In order to achieve this type of sharing the view model instance between the Activity and the Fragments, they must request the view model by passing the Activity reference while getting it:

ViewModelProviders.of(activity, vmFactory).get(...)

Testing

Prepare dependencies

Normally MainViewModel uses a GitHubClient implementation that calls the GitHub API using Retrofit HTTP client. In tests you would probably prefer to either mock the server (e.g. with MockWebServer) or just the GitHubClient implementation so that it won’t make the calls at all. In this example I’m going to use the latter approach (but testing the calls to a mocked server is also a good idea and you can do it separately).

Mock API client

The mocked implementation of the GitHubClient is very simple. Its constructor accepts a response it should return, an optional error it should throw instead of the response and a scheduler so that we can control the exact moment the data/error is returned. The error and the response are mutable properties so we can adjust them just before calling getRepo method.

import com.azabost.simplemvvm.net.response.RepoResponse
import io.reactivex.Observable
import io.reactivex.Scheduler

class MockGitHubClient(
        val scheduler: Scheduler,
        var repoResponse: RepoResponse = RepoResponse(1),
        var error: Throwable? = null
) : GitHubClient {

    override fun getRepo(owner: String, repo: String): Observable<RepoResponse> {
        val response = error?.let { return Observable.error(it) } ?: Observable.just(repoResponse)
        return response.subscribeOn(scheduler).observeOn(scheduler)
    }
}

Setting things up

I put all the MainViewModel tests in the MainViewModelTests class. It has a few properties:

  • MainViewModel
  • TestObservers for every Observable exposed by MainViewModel
  • MockGitHubClient
  • TestScheduler that is passed to MockGitHubClient

They are initialized in the setup method.

TestObservers record events passed by Observables and they allow to make assertions about them e.g. if a value has been emitted and what it was exactly.

TestScheduler controls the time when MockGitHubClient emits responses so that we can defer it and test what happens before the subscription completes (e.g. the progress animation should be still visible).

class MainViewModelTests {
    lateinit var vm: MainViewModel
    lateinit var gitHubClient: MockGitHubClient
    lateinit var testScheduler: TestScheduler
    lateinit var progressObserver: TestObserver<Boolean>
    lateinit var errorObserver: TestObserver<Int>
    lateinit var showDataObserver: TestObserver<Unit>

    @Before
    fun setup() {
        testScheduler = TestScheduler()
        gitHubClient = MockGitHubClient(testScheduler)
        vm = MainViewModel(gitHubClient)
        setupObservers(vm)
    }

    fun setupObservers(vm: MainViewModel) {
        progressObserver = TestObserver.create()
        vm.progress.subscribe(progressObserver)
        errorObserver = TestObserver.create()
        vm.errors.subscribe(errorObserver)
        showDataObserver = TestObserver.create()
        vm.showData.subscribe(showDataObserver)
    }

Writing some tests

Below you can find three simple examples.

Example 1

Test if positive response from the API triggers showData Observable and stores the data for later usage in the data variable.

@Test
fun getRepoShouldShowData() {
    val data = RepoResponse(12345)
    gitHubClient.repoResponse = data

    vm.getRepo("any", "thing")
    testScheduler.triggerActions()

    showDataObserver.assertValueCount(1)
    vm.data.shouldEqual(data)
}

Note: the shouldEqual method comes from ShouldKO which I really recommend but you can use any assertions you like.

Example 2

Test if HTTP exception triggers error Observable with the HTTP-specific error message.

@Test
fun getRepoErrorShouldShowHttpError() {
    gitHubClient.error = HttpErrors.getHttpException(404)

    vm.getRepo("any", "thing")
    errorObserver.assertValue(HttpErrors.DEFAULT_HTTP_ERROR_MESSAGE)
}

Example 3

Test if calling getRepo triggers progress Observable twice so that it should tell the view to show the loader and then to hide it.

@Test
fun getRepoShouldShowProgress() {
    vm.getRepo("any", "thing")
    progressObserver.assertValue(true)

    testScheduler.triggerActions()
    progressObserver.assertValueSequence(listOf(true, false))
}

Conclusion

As you can see, using ViewModels and RxJava Observables gives a very simple way to write unit tests for your code. I believe this great possibility will also encourage you to extract the business logic from the Android application components like Activities so that it can be tested without using instrumented tests or mocking the platform (e.g. with Robolectric).

This article was originally published on Bright Inventions blog.

0 comments on “Testing Android ViewModelsAdd yours →

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.