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.