Your view model class usually has several functionalities. It somehow fetches data, it optionally maps your data entities to displayable entities, handles clicks and performs action for given events. You want to be sure that after refactor your code will still work perfectly. To achieve that you need to write and maintain unit tests. I will show you simple way of creating unit tests for Android ViewModel implementation with step-by-step explanation and Test-Driven-Development spirit.
Prerequisites
In his example we will try to implement VenueDetailsViewModel in TDD style. TDD (Test Driven Development) is development technique, where developer relies on unit tests created before actual implementation.
Tools
I’ll be using Mockito and Mockito-Kotlin for mocking:
https://github.com/nhaarman/mockito-kotlin
And Strikt for assertions:
https://github.com/robfletcher/strikt
Classes in package
Let’s consider screen which displays some venue details and allows user to mark that place as favorite. We have following constructions for our view model:
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
)
Repository fetches data from remote API and emits value or error and FavoriteVenueController can check if venue is already marked by user as favorite and change its favorite state in local storage.
Note that every component is described by an interface — at a moment we don’t care about real implementations.
Initial code
Our initial code is class extending androidx.lifecycle.ViewModel
with some fields that are ready to be bound into layout. We also have start()
method that will be invoked just after ViewModel is available to be used and favoriteClick()
to handle click on favorite icon on screen. We also have errorCallback
— function which will be implemented in the higher class (e.x Activity) to display error message in Snackbar or Toast.
import androidx.lifecycle.ViewModel
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() {
}
fun favoriteClick() {
}
}
As you can see, I wrote createViewModel()
factory method with default values:mock<>
from Kotlin-Mockito extensions. It will be useful while writing single test cases, to not invoke full constructor each time.
import com.nhaarman.mockito_kotlin.mock
class VenueDetailsViewModelTest {
private fun createViewModel(
venueId: String = "fake_id",
venueRepository: Repository<VenueDisplayable> = mock(),
favoriteVenueController: FavoriteVenueController = mock()
) = VenueDetailsViewModel(
venueId = venueId,
venueRepository = venueRepository,
favoriteVenueController = favoriteVenueController
)
}
First test case
Let’s write our first test case. First of all, our ViewModel should display VenueDisplayable
from repository. We will cover two basic cases — case when Venue is found and second when error is emitted.
import com.nhaarman.mockitokotlin2.doReturn
import com.nhaarman.mockitokotlin2.mock
import io.reactivex.Single
import org.junit.Test
class VenueDetailsViewModelTest {
@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)
}
)
}
@Test
fun `given repository emits error when start view model then show error`() {
val viewModel = createViewModel(
venueRepository = mock {
on { getOne(FAKE_ID) } doReturn Single.error(Throwable("some error"))
}
)
}
private fun createViewModel(
venueId: String = FAKE_ID,
venueRepository: Repository<VenueDisplayable> = mock(),
favoriteVenueController: FavoriteVenueController = mock()
) = VenueDetailsViewModel(
venueId = venueId,
venueRepository = venueRepository,
favoriteVenueController = favoriteVenueController
)
companion object {
const val FAKE_ID = "fake_id"
val fakeVenue = VenueDisplayable(
id = FAKE_ID,
location = "Fake City, Main Square 1",
name = "Fake Place"
)
}
}
Now it’s time to create assertion:
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)
}
When you hit run test
then you should have two tests failing, because no implementation is present yet. So lets write simple start
method in our view model:
import androidx.lifecycle.ViewModel
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()
}
fun favoriteClick() {
}
}
There are more ways to achieve that behavior — your unit tests should follow arrange-act-assert flow or given-when-then.
Arrange — create objects, implement every stuff that is
given
.
Act — run your test method (in our case
start()
) —when
.
Assert — check if desired change of state happened or verify if some mocked object was invoked with proper parameters —
then
.
More test cases!
Now our tests will pass. Let’s create more test cases, for setting venue favorite
status:
@Test
fun `given venue favorite call returns favorite when start view model then set favorite`(){
val viewModel = createViewModel(
favoriteVenueController = mock {
on { checkIsFavorite(FAKE_ID) } doReturn Single.just(true)
}
)
viewModel.start()
expectThat(viewModel.isFavorite).isTrue()
}
If you run this test you will probably get NullPointerException
on venueRepository.getOne(venueId)
. That’s because we didn’t provided any stub for that method call. We already created in our tests default implementations for all class dependencies, but Mockito won’t handle default return values for us. We will add specific default implementation for all cases in our factory method. For stubbing rx calls I usually make use of .never()
method:
private fun createViewModel(
venueId: String = FAKE_ID,
venueRepository: Repository<VenueDisplayable> = mock {
on { getOne(any()) } doReturn Single.never()
},
favoriteVenueController: FavoriteVenueController = mock()
) = VenueDetailsViewModel(
venueId = venueId,
venueRepository = venueRepository,
favoriteVenueController = favoriteVenueController
)
Now our tests won’t throw NullPointerException
. Instead there will be simple assertion fail. After that we can implement our functionality, and of course other test cases for checking if venue is favorite.
@Test
fun `given venue favorite call returns favorite when start view model then set favorite`() {
val viewModel = createViewModel(
favoriteVenueController = mock {
on { checkIsFavorite(FAKE_ID) } doReturn Single.just(true)
}
)
viewModel.start()
expectThat(viewModel.isFavorite).isTrue()
}
@Test
fun `given venue favorite call returns not favorite when start view model then set not favorite`() {
val viewModel = createViewModel(
favoriteVenueController = mock {
on { checkIsFavorite(FAKE_ID) } doReturn Single.just(false)
}
)
viewModel.start()
expectThat(viewModel.isFavorite).isFalse()
}
@Test
fun `given venue favorite call error when start view model then don't set any value`() {
val viewModel = createViewModel(
favoriteVenueController = mock {
on { checkIsFavorite(FAKE_ID) } doReturn Single.error(Throwable("some error"))
}
)
viewModel.start()
expectThat(viewModel.isFavorite).isNull()
}
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() {
}
}
Of course we need to update our createViewModel
factory method to return default value for venue controller too, so our previous test cases won’t throw NullPointerException
.
private fun createViewModel(
venueId: String = FAKE_ID,
venueRepository: Repository<VenueDisplayable> = mock {
on { getOne(any()) } doReturn Single.never()
},
favoriteVenueController: FavoriteVenueController = mock {
on { checkIsFavorite(any()) } doReturn Single.never()
}
) = VenueDetailsViewModel(
venueId = venueId,
venueRepository = venueRepository,
favoriteVenueController = favoriteVenueController
)
Now we have one thing left — handling favoriteClick
.
First, lets write tests in given-when-then
manner:
Please note — in first test case I could’ve mock on <strong>{ </strong>checkIsFavorite(FAKE_ID) <strong>} </strong><em>doReturn </em>Single.just(false)
, but it wouldn’t be unit test anymore since it would be bound with other components. That’s not desired behavior all the time. In that way, when something in the class breaks, you will have clear information which singular piece of code failed.
Now when we wrote our test cases, we can proceed with implementation:
fun favoriteClick() {
isFavorite?.let { favorite ->
favoriteVenueController.markAs(venueId, !favorite)
.doOnComplete {
isFavorite = !favorite
}
.doOnError {
errorCallback.invoke(it)
}
.subscribe()
}
}
Nothing too fancy — it passes test cases we wrote. Code prettifying can be done later.
Final code
We finally covered all major test cases, our tests are unit tests, they do not depend directly on implementation and are relatively fast. Let’s see how final code looks like:
And our implementation:
We can do much more in here. We should inject somehow proper Schedulers
, we can indicate progress loading in some way, we can optimize repository calls and so on. We should also remember about disposing every call in onCleared
method from ViewModel
.
With this TDD-like process we were able to write tests that do not depend strongly on direct implementation. In that way, when you would someday change RxJava calls to Kotlin Coroutines, your base test cases won’t change.
Tips & tricks:
- try to think in
given-when-then
pattern — it will help you create cleaner code - test behavior, not internal implementation — in that particular view model we were asserting view model state (
isFavorite, VenueDisplayable
) and interaction (checking iferrorCallback
was invoked).
We shouldn’t make verification ifvenueRepository
was called with proper parameters or check iffavoriteVenueController
was invoked. That would cause more harm than good. - make use of Kotlin default values — we created
createViewModel()
method with default implementations — thanks to that our test cases are smaller and more readable - keep one check per test method — in that way it you will immediately know which component of your code is failing.
I believe you found something interesting in this article and you are ready to try something new while writing or refactoring features of your app. Good luck!
Check my other publications about unit testing:
Kotlin testing libraries, frameworks and utilities with examples
https://github.com/rozkminiacz/KotlinUnitTesting
Testing with Kotlin (Mobilization Conference 2018 talk)
Originally published on: https://proandroiddev.com