fbpx

Building Unit Tests for ViewModel in TDD style


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 if errorCallback was invoked).
    We shouldn’t make verification if venueRepository was called with proper parameters or check if favoriteVenueController 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


more insights

Uncategorized
Jarosław Michalik

3 things that defined my Android dev career

Building an Android developer career isn’t just about coding—it’s about the experiences that push you to grow. Whether it’s connecting with other Android devs at meetups or mastering the fundamentals, it’s those key moments that shape how you approach the craft.

Read More »

AndroidPro newsletter 🚀

join 3057 developers reading AndroidPro newsletter every week

👉 tips & tricks, articles, videos

👉 every Thursday in your inbox

🎁 bonus on signup – Clean Architecture blueprint

brought to you by Jarek Michalik, Kotlin GDE

You can unsubscribe any time. For details visit our privacy policy.

android pro newsletter tips and tricks how to learn android