Android MVVM Testing Patterns

When does a mobile app become enterprise?

When it contains thousands of lines of business logic?
Or when it brings good profit?

I stick to the idea that solid test coverage makes a mobile app enterprise. At the company where I work, developers spend no less efforts writing tests than writing business logic.

Couple of my colleagues wrote articles on MVVM which I think would be a great read and a pre-requisite to this article which focuses on some testing strategies you could follow when writing your Android app.

Here’s the articles by the way:

Let’s start with an example that summarizes what components a single screen of MVVM app consists of (taking the app I’m working on as an example):

  1. ViewModel
  2. View
  3. Fragment/Activity
  4. Contract (optional)
  5. Model

We use Contract as a component that migrated from previous architecture we used (MVP). The Contract mostly serves a purpose to list obligations View and ViewModel would need to implement. Contract in MVVM is optional and could be omitted but we still keep it because it is a place for developers to quickly find all interactions between View and ViewModel so it gives a clear picture of screen functionality.

Here’s the (slightly modified) example of Contract class of the screen that is responsible for presenting list of products.

interface ProductListContract {

interface View {
var viewState: ViewState
fun initWithViewActionDelegate(viewActionDelegate: ViewActionDelegate)
fun updateCheckoutButton(checkoutButtonPayload: CheckoutButtonPayload)
fun showProducts(productList: PagedList<Product>)
}

interface ViewActionDelegate {
fun onCheckoutButtonClicked()
fun onSearch(query: String)
fun onSearchInactive()
fun onProductClicked(productId: Long)
}
}
Product List screen

The main points here are View interface which lists possible changes the view can receive (example: viewState which is responsible for different states of the view like empty state, search and some others). ViewActionDelegate interface lists all possible interactions user can perform with the View.

The first component here is ViewModel which implements Contract.ViewActionDelegate. Most common tasks it performs are fetching information from local database or server (or any other alternative data sources), map that data to a format View would understand and send it to the View to display.

class ProductListViewModel private constructor(
private val dataProvider: DataProvider,
private val cart: Cart,
private val backgroundExecutor: BackgroundExecutor
) : ViewModel(),
ViewActionDelegate {

private val _checkoutButtonPayloadLiveData = MutableLiveData<CheckoutButtonPayload>()
val checkoutButtonPayloadLiveData: LiveData<CheckoutButtonPayload> = _checkoutButtonPayloadLiveData

override fun onCheckoutButtonClicked() {
// implementation
}

override fun onSearch(query: String) {
// implementation
}

override fun onSearchInactive() {
// implementation
}

override fun onProductClicked(productId: Long) {
backgroundExecutor.execute {
val product = dataProvider.fetchProductWithId(productId)
product?.let {
cart.addProduct(product)
_checkoutButtonPayloadLiveData
.postValue(cart.toCheckoutButtonPayload())
}
}
}

internal class ProductListViewModelFactory(
private val dataProvider: DataProvider,
private val cart: Cart
) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return ProductListViewModel(
dataProvider,
cart,
BackgroundExecutor()
) as T
}
}
}

Just for a purpose of example I only kept one method implementation here which fetches selected product variant from database, adds it to the cart and sends an event to update checkout button with latest cart info (ex. total price of the cart).

Next component is View which implements Contract.View interface.

class ProductListView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
) : RelativeLayout(context, attrs, defStyleAttr),
View {

private lateinit var viewActionDelegate: ViewActionDelegate

override var viewState = ViewState.PRODUCTS
set(value) {
field = value
updateViewState()
}

override fun initWithViewActionDelegate(viewActionDelegate: ViewActionDelegate) {
this.viewActionDelegate = viewActionDelegate
adapter.productClickedListener = { productId -> viewActionDelegate.onProductClicked(productId) }
}

override fun updateCheckoutButton(checkoutButtonPayload: ProductListContract.CheckoutButtonPayload) {
with(checkoutButtonPayload) {
checkoutBarView.updateViews(itemQuantity, subtotal)
}
}

override fun showProducts(productList: PagedList<Product>) {
// implementation
}
}

The last piece is Fragment/Activity where View and ViewModel parts are connected together.

class ProductListFragment : Fragment() {

private lateinit var viewModel: ProductListViewModel
private lateinit var productListView: ProductListContract.View

// Injecting Model dependencies (ex. cartManager) is ommited

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
viewModel = ViewModelProviders.of(
this,
ProductListViewModel.ProductListViewModelFactory(dataProvider, cart)
).get(ProductListViewModel::class.java)
}

override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val view = inflater.inflate(R.layout.product_list_fragment, container, false)
// initialize view model
productListView = view as ProductListContract.View
// set action delegate for view to communicate with viewModel
productListView.initWithViewActionDelegate(viewModel)
bindViewModel()
return view
}

private fun bindViewModel() {
viewModel.checkoutButtonPayloadLiveData
.nonNull()
.observe(this) {
productListView.updateCheckoutButton(it)
}
}
}

As you can see from the code: ViewModel doesn’t know anything about the View, i.e. has no reference to the View. This is the reason why Contract is optional since it’s the place to conclude an agreement between parts.

One interesting method here is .nonNull() which comes from livedata-ktx library. One of its features is making observer’s argument to be non-nullable (by discarding all null events). If interested, here’s a quick link 😉

You made it to the second part of this article where I try to tell about actual testing patters. But before I thought this conference talk by Zac Sweers would be a perfect preface that’ll help to understand what are you writing tests for.

In order to clearly understand what kind of tests you need for every component of MVVM imagine them as black boxes.

In the Model component the business logic is a black box. Main principle here is to verify that valid input data is processed correctly according to the business rules into expected output data. All invalid or unexpected input data is also handled correctly.

In the ViewModel component the logic to prepare data for the UI is a black box. Often the input of the ViewModel is the output of Model so the main principle here is to verify that any possible data the model produces is converted and sent as output in a format so thee View can display it. If you look at this from the code perspective: any interaction with a ViewModel should produce expected LiveData event.

In the View component the black box is the UI logic, for example displaying text in a TextView or showing/hiding a View, etc. The input to the View is the output of the ViewModel and the output is the state of the UI (I like to imagine it as a screenshot of app screen).

It’s very important to understand borders between those components. When test one component you don’t need to have other components. For example, when you test the Model you don’t need to have any mentions of ViewModel and especially View in the test. Another example is ViewModel where you might think you need Model but you actually just need different outputs the model can product. To achieve that mocks come very useful since they just simulate behaviour of a real model which you can control.

Model and ViewModel tests are usually unit tests or JVM tests. In order to run them you don’t need any of the parts of Android framework. Because of that such tests are very fast and reliable. Try to cover as much as you can using those types of tests.

View tests are the different. The can’t be tested without Android framework. What it means is they can’t be run without emulator or real device. Such tests are slow, less reliable and very dependent on the environment they’re run in.
View tests are usually instrumented tests.

Note: in Android Studio project instrumented tests live in androidTest folder, test is for JVM tests

Now, assuming you know what are you testing, let’s start with couple tools of choice.

  1. Truth: nice API for assertions and very readable failure messages

2. Mockito

3. Mockito-Kotlin which provides a nice API for using Mockito in Kotlin (

First component to test (and the one this article will mostly be focused on) is going to be ViewModel. As I mentioned before, we’re going to interact with ViewModel and assert produced events that are sent via LiveData.

So here’re 3 steps every test case would contain of:

  1. Set up ViewModel’s dependencies, i.e. mock some Models that ViewModel will use.
  2. Interact with ViewModel, i.e. call its public methods (the ones that are listed in Contract).
  3. Observe LiveDatas and verify the data that comes with LiveData’s events

First, create a test class in test directory of your project. Note: you would always need to add InstantTaskExecutorRule because some Android Architecture Components execute tasks in background but we don’t want that in our tests. Then mock all dependencies ViewModel uses (they are passes as ViewModelFactory’s constructor arguments).

class ProductListVIewModelTest {

@Rule
@JvmField
var instantExecutorRule = InstantTaskExecutorRule()

private val dataProvider: DataProvider = mock()
private val cart: Cart = mock()
}

Second, interaction with ViewModel. I am going to test the part that is responsible for adding product to the cart when a product in the list is clicked and updating checkout button with new cart item quantity and cart subtotal.

This 🙂

As you could already guessed two logical pieces here to test are:
1) Product is added to the cart.
2) Event to update view state is sent

override fun onProductClicked(productId: Long) {
backgroundExecutor.execute {
val product = dataProvider.fetchProductWithId(productId)
product?.let {
cart.addProduct(product)
_checkoutButtonPayloadLiveData
.postValue(cart.toCheckoutButtonPayload())
}
}
}

However as you can see onProductClicked method does some additional actions here. Since we only need to worry about addition to the cart and _checkoutButtonPayloadLiveData‘s event and payload we don’t care about other things. dataProvider.fetchProductWithId is used to load product from the database but assuming there’re tests that fully cover database logic and give us confidence that fetching variant works we can mock it. toCheckoutButtonPayload() is an extension function of Cart class that call its two methods cartSize() and cartSubtotal() and creates a simple view data class that View knows how to present. Thus we also need to mock those two methods.

data class CheckoutButtonPayload(
val quantity: Int,
val subtotal: String
)

fun Cart.toCheckoutButtonPayload(): CheckoutButtonPayload {
return CheckoutButtonPayload(cartSize(), cartSubtotal())
}

The test case would look something like that:

@Test
fun `adding product to a cart returns one item quantity and subtotal`() {
val product: Product = mock()

whenever(dataProvider.fetchProductWithId(any())) doReturn product
whenever(cart.cartSize()) doReturn 1
whenever(cart.cartSubtotal()) doReturn "$5.00"
}

Then we need to pass those dependencies into ViewModelFactory to create a properly configured for that test case ViewModel and create mock Observer that is needed for verifying LiveData’s events.

@Test
fun `adding product to a cart returns one item quantity and subtotal`() {
val product: Product = mock()

whenever(dataProvider.fetchProductWithId(any())) doReturn product
whenever(cart.cartSize()) doReturn 1
whenever(cart.cartSubtotal()) doReturn "$5.00"

val viewModel = ProductListViewModel.ProductListViewModelFactory(
dataProvider,
cart
).create(ProductListViewModel::class.java)

val mockObserver: Observer<CheckoutButtonPayload> = mock()
viewModel.checkoutButtonPayloadLiveData.observeForever(mockObserver)

}

And finally we need to actually call onProductClicked method explicitly and verify that behaviour we expect to happen are actually happening. Mockito’s verify(mock: T) is used to verify the behaviour happens just once.

The complete test case would look like this:

@Test
fun `adding product to a cart returns one item quantity and subtotal`() {
val product: Product = mock()

whenever(dataProvider.fetchProductWithId(any())) doReturn product
whenever(cart.cartSize()) doReturn 1
whenever(cart.cartSubtotal()) doReturn "$5.00"

val viewModel = ProductListViewModel.ProductListViewModelFactory(
dataProvider,
cart
).create(ProductListViewModel::class.java)

val mockObserver: Observer<CheckoutButtonPayload> = mock()
viewModel.checkoutButtonPayloadLiveData.observeForever(mockObserver)

viewModel.onProductClicked(1L)

verify(cart).addProduct(any())
verify(mockObserver).onChanged(any())
}

Note the we don’t need to care about which exact product was clicked so we could pass any product ID as an arguments. We just verify that the interaction with ViewModel calls addProduct method once and observer registers one event sent by checkoutButtonPayloadLiveData.
Additionally we could assert correctness of the data too. Just change last two lines to:

verify(cart).addProduct(product) 
verify(mockObserver).onChanged(check {
assertThat(it.quantity).isEqualTo(1)
assertThat(it.subtotal).isEqualTo("$5.00")
})

Note: first line performs one assertion on the received argument. Using check function you could have multiple assertion on the received argument.

🎉the test passed (super fast)

As a second example we could write another test case here is to verify that clicking on product twice, i.e. calling onProductClicked twice would also perform two interactions with the cart and send two LiveData events.

@Test
fun `checkout button is updated on every product addition`() {
val product: Product = mock()
whenever(dataProvider.fetchProductWithId(any())) doReturn product

val viewModel = ProductListViewModel.ProductListViewModelFactory(
dataProvider,
cart
).create(ProductListViewModel::class.java)

val mockObserver: Observer<CheckoutButtonPayload> = mock()
viewModel.checkoutButtonPayloadLiveData.observeForever(mockObserver)

viewModel.onProductClicked(1L)
viewModel.onProductClicked(1L)
verify(cart, never()).deleteProduct(any())
verify(cart, times(2)).addProduct(any())
verify(mockObserver, times(2)).onChanged(any())
}

Using verify(mock: T, mode: VerificationMode) we could verify that behaviour happens at least once /exact number of times / never. As a bonus here we could also test that clicking on product never deletes anything from the cart. It seems quite obvious but what if!

To extend previous example we can change the test to verify that clicking different products items in the list adds different products in the cart. For this case we would use argument captors which will keep track of all the interactions with the mock. In this case clicking a product with id 1L will add first product in the cart, and then clicking a product with id 2L will add second product in the cart. Then we just verify captured interactions in the order they happened.

@Test
fun `checkout button is updated on every product addition`() {
val productOne: Product = mock()
val productTwo: Product = mock()

whenever(dataProvider.fetchProductWithId(1L)) doReturn productOne
whenever(dataProvider.fetchProductWithId(2L)) doReturn productTwo

val viewModel = ProductListViewModel.ProductListViewModelFactory(
dataProvider,
cart
).create(ProductListViewModel::class.java)

val mockObserver: Observer<CheckoutButtonPayload> = mock()
viewModel.checkoutButtonPayloadLiveData.observeForever(mockObserver)

viewModel.onProductClicked(1L)
viewModel.onProductClicked(2L)

verify(mockObserver, times(2)).onChanged(any())

argumentCaptor<Product> {
verify(cart, atLeastOnce()).addProduct(capture())

assertThat(allValues).hasSize(2)
assertThat(firstValue).isEqualTo(productOne)
assertThat(secondValue).isEqualTo(productTwo)
}
}

Those two and a half examples of tests should work for 99% of cases when you write tests for your own view models if you implemented them similarly to what you see in this article (or in Google’s ViewModel tutorials).

Next component to test is View. I’m less opinionated about perfect tool for testing here so I’ll just list couple and give some tips on how to make View testing easier.

The general idea is to implement a View so it can have a deterministic set of visual states including the default state. Just view inflation (without any data sent to it) should look decent enough to present to a user without feeling shame. Individual sub-views could have their own states but changing those states should not change the state of a parent view. They can however affect other sub-view. My explanation might not be perfect but if you can strip the sub-view from the parent view and it should still work, look nice and be testable. Just stick to a composition principle and you’ll be ok. Next , tools:

  1. Espresso

TL;DR Good but not the best. Very easy to imitate user interactions (button clicks, text input, etc.) but harder to see the test output. In other words, you can tell based on assertion whether a particular view is visible or not, but if you want to see how the view looks overall you might use something different, like screenshots.

2. Screenshot testing libraries

Disclaimer: I haven’t tried facebook library but the description looks promising.

At Shopify we use in-house built screenshot library which is has a quite similar idea. It combines espresso actions like view clicks and text inputs and screenshot asserting functionality. When you write a test, you record a baseline screenshot image which is then saved somewhere in the androidTest/assets folder. Next time you run the test case it generates new image and compares it with a baseline pixel by pixel. Non-identical images fail test. The downside is that screenshot tests are very sensitive to the environment they run at so they could start failing if you change size of the emulator or Android version. It could also fail if the OS has different GPU drivers so SVGs are rendered differently. Or tens of other wild random reason.

Last component to test in this article is Model. I intentionally left it until the end because this is the heaviest one. However, I won’t cover how write unit tests for model layer for two reasons: there’s plenty of great articles online ( Android testing) and I don’t want to embarrass myself feeling how little I know about it 😄). So good luck with that!

This is the end of the article. Thanks for spending your time reading it and let me know if I’m wrong somewhere or if there’s something that could do differently or better. I’m open to your feedback 🙌🏻

Mobile developer, snowboarder, dog lover

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store