Categories
Android Apps Architecture testing

Part 2: Testing with MockK and Koin

One of the best things about MVVM is the use of separation of concerns principle which by design enables testing of each component in isolation. View, ViewModel and Model are all separated and thus easily testable.

When thinking of testing, then unit testing comes to mind first and for that mocking of dependencies is required.

Mockito vs MockK

After investigating Google’s demo project, it seemed Mockito was the way to go with mocking and verifying tests.

What I soon realised, was that it wasn’t the most convenient library to use in Kotlin, and I also had problems with just getting it to work. I then discovered MockK, written in Kotlin, which was easy to setup and thus made a perfect choice for my project.

Consider mocking a network response:

Mockito

val call = successCall(contributors)
`when`(service.getContributors()).thenReturn(call)

MockK

val call = successCall(contributors)
every { service.getContributors() } returns call

I choose MockK’s every / lambda style over Mockito’s `when`.

The only thing missing from MockK is verifying constructor calls, for which there is a Github issue. Because of this I needed to refactor my code and inject the dependencies, instead of constructing them.

MockK setup

Separate libraries are required for unit and instrumentation tests:

testImplementation "io.mockk:mockk:$version"
androidTestImplementation "io.mockk:mockk-android:$version"

This is enough to start mocking dependencies in Unit tests. For instance, mocking a network client:

val client = mockk<RepoClient>()

For instrumentation tests, you need to launch the real activity and thus need a separate Test App class and mocked Koin modules. Read about this in Instrumentation section below👇🏽👇🏽

Testing the Repository

All the classes can be covered with unit tests. In the @Before block, you should create the class with mocked dependencies:

@Before
fun before() {
    client = mockk<RepoClient>()
    repository = RepoRepository(client, more mocks..)
}

Then, in your test, you can mock answers from your dependencies and verify expected Repository behavior.

// mock the repository observer
val observer = mockk<Observer<Resource<List<Repo>>>>()
// call getRepos() and observe the response
repository.getRepos().observeForever(observer)
// verify repos are fetched from network
verify { client.getRepos() }
// simulate that network data was stored to db
dbData.postValue(repos)
// verify getRepos() observer was called
verify { observer.onChanged(Resource.success(repos)) }

With this style you can write unit tests for all of your classes.

UI Instrumentation tests

Instrumentation tests are used to verify what is visible to the user. The app will launch with mocked ViewModel and the tests can then verify the UI state.

Setup

For instrumentation tests, you have to set up a custom Test App and its companion Test Runner which is then used to run the tests. Needless to say the setup is pretty complicated but the tests are worth it after the initial hurdle.

@Before and @After

Before the UI test, the ViewModel should be mocked and its responses set. Then the tested activity/fragment should be launched.

@Before
fun before() {
    // mock the ViewModel
    loginRequest = MutableLiveData()
    loginViewModel = mockk(relaxed = true)
    every { loginViewModel.user } returns loginRequest

    module = module(true, true) {
        single { loginViewModel }
        // mock other dependencies
        single { mockk<MainViewModel>(relaxed = true) }
        ... etc
    }

    loadKoinModules(module)
    // launch the activity
    scenario = launchActivity()
}

After the test the activity should be closed and Koin modules unloaded so the next test can start with cleared objects.

@After
fun after() {
    scenario.close()
    unloadKoinModules(module)
}

The test

Then, as in Unit tests, you can mock updates from ViewModel and verify expected View behavior. You can also simulate input from the view.

For instance, verifying a Toast message after invalid login:

// input wrong credentials
inputCredentials("wrong", "wrong")

// click the login button
onView(withId(R.id.loginButton)).perform(click())

// verify ViewModel's login is called
verify { loginViewModel.login(any(), any()) }

// simulate error response
loginRequest.postValue(Resource.error(getString(R.string.invalid_credentials), null))

// assert error toast shown
onView(withText(R.string.invalid_credentials))...

Similarly to this, all of the views can be tested.

Conclusion

Although setup and complexity of Android tests could be improved, it is essential for delivering a quality application.

I can say from my experience that numerous times seemingly irrelevant tests have failed after writing new code. These failed tests, if not caught, would have meant bugs in production.

Reference

Please have a look at the unit and instrumentation tests in the sample project’s source code.

Categories
Android Apps Architecture

Android App Architecture, Part 1

Not being familiar with modern Android App architecture and coming from the ViewController world, it is confusing jumping into the recommended MVVM architecture. There are also some parts of the official guide that are left for the reader to figure out, so I will write about my experience with implementing it in my repo browser project.

Choosing dependencies

Besides native Architecture dependencies, there are some parts of the setup that are not defined in the guide. Two of the main ones are the networking and dependency injection libraries.

Networking

The guide uses Retrofit for networking. For me, it made sense to use Volley. I like that I’m in control of the requests that I write, and am not dependent on the Retrofit abstraction of mapping requests to data objects. I can be sure that if there will be customisation required for my requests, Volley can handle it.

Dependency injection

There are many DI libraries, with the most popular one being Dagger. For me, it seemed complicated to get started with and with a lot of boilerplate. I didn’t see any drawbacks from using Koin, so I went for that one instead.

Here are the final dependencies for my project.

Koin setup

After planning and creating the required Activity, ViewModel, Networking, Database and Repository objects, it was time to set up the Koin modules. For my use case, I created singletons and viewModels:

single { get<AppDatabase>().repoDao() } // repository Database
single { RepoClient(context, get()) } // repository networking
single { RepoRepository(get(), get(), get()) } // repository repository to merge data from database and network
viewModel { (handle: SavedStateHandle) -> RepoListViewModel(handle, get(), get()) } // the viewModel

Notice the viewModel with SavedStateHandle argument. This allows access to the saved state and arguments of the associated Activity.

Other singletons required were: Thread executors, Room database, SharedPreferences

Here is the Koin setup.

Repository setup

For fetching the repositories from Github and storing/reading them from the database, 3 components were required:

  • Network client
  • Database object
  • MediatorLiveData to merge network/db data

After that, accessing the data in LiveData format is straightforward:

val repoResource = object : NetworkBoundResource<List<Repo>, List<Repo>>(executor) {
override fun saveCallResult(item: List<Repo>) = repoDao.save(item)

override fun shouldFetch(data: List<Repo>?): Boolean {
return data == null || data.isEmpty() || rateLimit.shouldFetch("repos")
}

override fun loadFromDb() = repoDao.getRepos()

override fun createCall() = repoClient.getRepos()

override fun onFetchFailed() = rateLimit.reset("repos")

}.asLiveData()

Check out the repository source code.

Populating the view

For the last part, the view is updated according to the LiveData<List<Repo>> result. If there is data, the ListView is populated with a DataBinding adapter:

viewModel.repos.observe(viewLifecycleOwner) {
    // update UI
    it.status.showWhenLoading(listProgressBar)

    when (it.status) {
        Status.SUCCESS -> {
            adapter.submitList(it.data)
        }
        Status.ERROR -> {
         Toast.makeText(this@RepoListFragment.activity, it.message, Toast.LENGTH_LONG).show()
        }
    }
}

Conclusion

These are the main steps of creating an Android MVVM skeleton app. Of course there are more details(networking, database), which can be discovered in the GitHub repository.

Stay tuned for Part 2, where I will write about testing the app with Mockk.

Reference

Project code in GitHub.