Let’s start not from programming, but from the home construction industry. We will deal with windows in houses or apartments. In such windows we usually find handles – the handle should open the window. The window system with the handle can be checked in several ways, including:
- Does the handle fit the given window doors?
- Does the handle, when turned, move the elements of the lock?
- Does the handle, when turned, unlock the window?
The analogy is justified only by a popular gif.
Let’s now translate these requirements into programming world. Let the window system be our app.
- Does the handle fit the window frame? This information should be provided by the compiler.
- Does the handle move the window fittings when turned? We will check this with a unit test.
- Does the handle open the window when turned? Here an integration test will help us. Or a differently constructed unit test. It depends on what we take as our “unit” in the given test.
- And now finally – are we able to open the window? Here is a case for end-to-end tests.
And what if we have many windows and such cases arise as in the gif above?
Well, in such a situation we cannot afford for the handle to work badly. The rest of the house must be built.
The programming community is familiar with the concept of the test pyramid. This is a graphical representation of the desired amount of tests of a certain type in a project.
The base of the pyramid are unit tests, and the higher up, the more “integrated” the tests are, until they reach the level of testing the entire application end-to-end.
A lot of unit tests give us a certain peace of mind. Our project is burning, but at least we are sure that that little class works as we wish and that no pre-deployment hotfixes have spoiled it.
What is a unit test?
A unit test is a test that meets the following criteria:
- It only checks one element of system behavior
- It is isolated
- It works deterministically
Imagine our code is like a puzzle. Many classes or other components come together to form a complete picture of the application.
The green puzzle is our “system under test”. We will focus on it. In an ideal case, this block does not connect directly with others, so in a unit test we can assume such a scheme.
We will use mocks. Such components that fit our class, but have no specific implementations. We will control the behavior of the mocks and check how the tested component will behave when other puzzles give it different data.
What are we testing on Android?
What components of the Android app will we cover with unit tests? All business logic. You can say it differently – domain code. We describe the behavior of our application in the Clean Architecture approach using Use Cases, Interactors, Repository layers, Services, and finally Presenter or ViewModel layers.
Note – we will unit test those components that have no Android imports, or have the necessary minimum. Ultimately, we will also strive to make the project layers containing application logic as isolated from the Android framework as possible.
Let’s see the code
Let’s try to implement a test for a certain view model. Here is the VenueDetailsViewModel and a few accompanying classes:
class VenueDetailsViewModel(
val venueId: String,
val venueRepository: Repository<VenueDisplayable>,
val favoriteVenueController: FavoriteVenueController
) : ViewModel() {
var isFavorite: Boolean? = null
var displayable: VenueDisplayable? = null
var errorCallback: (Throwable) -> Unit = {}
fun start() {
venueRepository.getOne(venueId)
.doOnSuccess {
displayable = it
}
.doOnError {
errorCallback.invoke(it)
}
.subscribe()
favoriteVenueController.checkIsFavorite(venueId)
.doOnSuccess {
isFavorite = it
}
.doOnError {
// ignore
}
.subscribe()
}
fun favoriteClick() {
}
}
interface Repository<T> {
fun getOne(id: String): Single<T>
}
interface FavoriteVenueController {
fun markAs(venueId: String, favorite: Boolean): Completable
fun checkIsFavorite(venueId: String): Single<Boolean>
}
data class VenueDisplayable(
val id: String,
val name: String,
val location: String
)
What is our ViewModel supposed to do?
- Retrieve a Venue with a specific ID from the Repository.
- To display the Venue data on the screen – that is, assign it to the VenueDisplayable field.
- If something bad happens, it should give notice of it – that is, call the errorCallback function.
Let’s create test class and add builder function for ViewModel:
class VenueDetailsViewModelTest {
private fun createViewModel(
venueId: String = "fake_id",
venueRepository: Repository<VenueDisplayable> = mock(),
favoriteVenueController: FavoriteVenueController = mock()
) = VenueDetailsViewModel(
venueId = venueId,
venueRepository = venueRepository,
favoriteVenueController = favoriteVenueController
)
}
Attention. Very important thing. We do not provide specific implementations of Repository, Controller, or other things for unit tests. We provide test doubles, most commonly called mocks for short. For mocking I use Mockito, and in Kotlin projects more and more often MockK.
In this case, in the createViewModel() function I pass default arguments – empty mocks created using Mockito with Kotlin Extensions.
Let’s finally implement the test.
In our first test case, we will check if the VenueDisplayable obtained from the venueRepository is actually assigned to the displayable field, and in the second case we will check if an error is passed to the errorHandler function in case of an error.
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import com.nhaarman.mockitokotlin2.verify
import io.reactivex.Single
import org.junit.Test
import strikt.api.expectThat
import strikt.assertions.isEqualTo
@Test
fun `given repository emits value when start view model then display that value`() {
val viewModel = createViewModel(
venueRepository = mock {
on { getOne(FAKE_ID) } doReturn Single.just(fakeVenue)
}
)
viewModel.start()
expectThat(viewModel.displayable).isEqualTo(fakeVenue)
}
@Test
fun `given repository emits error when start view model then show error`() {
val mockErrorCallback: (Throwable) -> Unit = mock()
val throwable = Throwable("some error")
val viewModel = createViewModel(
venueRepository = mock {
on { getOne(FAKE_ID) } doReturn Single.error(throwable)
}
).apply {
errorCallback = mockErrorCallback
}
viewModel.start()
verify(mockErrorCallback).invoke(throwable)
}
Could I include these two cases in one test? Probably yes. But that’s not the point here. We want to make sure that error display works regardless of whether data display is working properly. It is worth having one assertion in the test, if only for the way most test frameworks work. Usually after the first failed assertion, the rest of the test is interrupted. I wrote here about how to have several assertions in one test without unnecessary pain.
What is also extremely important in a unit test? Isolation.
Note that this test does not care whether the Repository<> is a database or some network source. This test does not care about implementation details. Just as well, the Repository may not work. ViewModel just gets along well with the Repository interface, and that’s important in unit tests. On the other hand, we have some errorCallback. It will be implemented in the view layer as a Toast or Snackbar. But is this important from the unit test perspective? It is not important at all.
Short FAQ at the end:
How to achieve test isolation?
Dependency injection for the win. Follow the dependency inversion philosophy and you’ll be home.
How to choose a unit test?
Choose the most basic behavior of the system that comes to your mind. It may be one public method, it may be a very specific program path.
Which framework to choose for unit tests on Android?
Junit5, Kotest, MockK. That’s a good base. I think Junit4 should not be used in new projects anymore. If you already feel comfortable with unit tests and want to try a different style – check out Kotest and its Specs. For mocking – if you are already using Mockito, it’s a good idea to pull in additional Kotlin extensions for this library. And if you heavily use coroutines, it’s a good idea to fire up MockK.
Where to learn good practices in testing on Android in Kotlin?
It’s best to follow the news on the KotlinTesting website. Sign up to newsletter and you won’t miss updates of free materials (like this one) and get the best offer on premium educational programs.