Android: Model-View-ViewModel and how to test it

May 22, 2019, 11:04 p.m.
By: silverbaq
Android Test Kotlin Programming

Introduction


At this point in time, it's no news that MVVM is the architecture which Google is recommending as the architecture of choice for Android application.  The Android JetPack (link) project contains components that will help you make architecture come into play in your application. At the time, there is already a ton of articles on how to use MVVM. But honestly, it took me quite some time to wrap my head around, who to actually work with it correctly. Not to mention how to test it!

Therefore, I have decided to make a very simple Android application, consisting of a TextView and two buttons. One button to increment a number, the other to decrement and the TextView to display the number. With this application, I will explain how to build it with Android JetPack to get it to use MVVM as it's architecture - And how to test it of course.

The application

To begin with, I've created a new application in Android Studio, using Androidx and with an empty Activity - NB! "Androidx... Uhh, Fancy!"

Since I wish the application to be as easy to test as possible (and just testable in general), I will go with the principles of clean architecture (link)

I'll have an Android Fragment will be the View. A ViewModel to be the ViewModel (Ohh Realy?) and I'll make a CountRepository that will handle the model. Since it's "just" an integer which is the data in this application, I will not be making a proper model. But the concept is exactly the same when working with something which is more complex.

Dependencies

To make all this happen, we will be needing a couple of dependencies to some libraries that will help us make our dreams come true.
In the app build.gradle file we will be adding:


dependencies {
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-alpha01'

    testImplementation "com.nhaarman.mockitokotlin2:mockito-kotlin:2.1.0"
    testImplementation 'org.mockito:mockito-inline:2.13.0'

    testImplementation 'androidx.arch.core:core-testing:2.1.0-beta01'
    androidTestImplementation 'androidx.test:core:1.1.0'
}


What have I added

  • androidx.lifecycle
    • lifecycle-extensions: For having the ViewModel and LiveData components from JetPack
  • com.nhaarman.mockitokotlin2
    • mockito-kotlin: For mocking objects in our tests
  •  org.mockito
    • mockito-inline: So we can mock our Repository, without getting 'fianl class exception'
  • androidx.arch.core
    • core-testing: So we can have InstantTaskExecutorRule for our tests
  • androidx.test
    • core: The core library for tesing in Androidx


Getting started coding

Android Studio now comes with a feature to create the basic setup for a Fragment with ViewModel (Right click a package -> new -> Fragment -> Fragment (with ViewModel)). This will create the three files (A ViewModel file, a fragment file and its XML file for the layout) we will be needing to begin with.

I'll start out with making the layout, mostly because it will just be out of the way for now :-P

The layout

As said earlier, I will just have a TextView and two Buttons as the layout. So, nothing out of the ordinary to explain here.


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".oneup.OneUpFragment">


    <Button
            android:text="Add"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/btnAdd" app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintEnd_toEndOf="parent" android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/tvDisplay"/>
    <Button
            android:text="Sub"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:id="@+id/btnSub" app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" android:layout_marginTop="8dp"
            app:layout_constraintTop_toBottomOf="@+id/btnAdd"/>
    <TextView
            android:text="0"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/tvDisplay" android:layout_marginTop="32dp"
            app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"
            android:textSize="36sp"/>

    android:layout_width="match_parent"
            android:layout_height="match_parent">

</androidx.constraintlayout.widget.ConstraintLayout>


The Model (Well... Repository...)

Now that we have the layout out of the way. Let's concentrate on the actual application (Because layout has nothing to do with the application... Everyone knows that?!). I'm not going to take a TDD approach to this since the problem is very simple (increment/decrement an integer), and how I will be doing this is very well know.


import androidx.lifecycle.MutableLiveData

class CountRepository {
    val counterLiveData = MutableLiveData(0)

    fun sub() {
        val count = counterLiveData.value
        counterLiveData.postValue(count?.minus(1))
    }

    fun add() {
        val count = counterLiveData.value
        counterLiveData.postValue(count?.plus(1))
    }
}

My CountRepository consists of a MutableLiveData, which will have the default value of 0. Without doing into too many details about how LiveData works, the main idea is to have a value that is observable. So when it changes, everyone observing the value will be able to adjust to the changes.

There are also two functions to interact with and so the repository can do its logic and update the counter.

Testing the Model (Well... Repository...)

In the test package (not to be confused with the androidTest) I will create a class for the tests.

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import dk.w4.mydemoapplication.oneup.CountRepository
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test

class CountRepositoryTest {

    @get:Rule
    val rule = InstantTaskExecutorRule()

    val countProviter = CountRepository()

    @Test
    fun `CountProviter count is 0 on init`(){
        assertEquals(0, countProviter.counterLiveData.value)
    }

    @Test
    fun `CounterProviter can sub one`(){
        countProviter.sub()
        assertEquals(-1, countProviter.counterLiveData.value)
    }

    @Test
    fun `CounterProviter can add one`(){
        countProviter.add()
        assertEquals(1, countProviter.counterLiveData.value)
    }
}

The tests them self, are very basic unit tests. I do not expect that these need further explanation. The interesting part here is that we are testing LiveData. To make sure the tests will run sequentially as expected, it is needed to have an instance of InstantTaskExecutorRule() as the rule for this test. If we did not have this rule, the tests would fail (except the "CountProviter count is 0 on init" test) since we would get an `java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked.` exception.

The ViewModel

Now, as the name says tells - This is the key element in the entire architecture. The ViewModel should handle all the logic for the view, without being android specific things. That means, not to set the elements in the layout, but to do the logic which will be shown in the layout. The same goes of stuff like the Intents, Network communication, SystemManager and so on. These should either be handled by Presenters/Controllers, Fragment/Activity or something that makes sense.

import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel

class OneUpViewModel(private val countRepository: CountRepository) : ViewModel() {

    fun observeCounter(): LiveData<Int> = countRepository.counterLiveData

    fun addToCounter() { countRepository.add() }

    fun subFromCounter() { countRepository.sub() }
}

Since this application is extremely simple in its functionality, I am also very few lines of actual code.
The OneUpViewModel class extends the ViewModel class, which is the JetPack component, making the OneUpViewModel able to store and manage UI-related data in a lifecycle conscious way. So, if the device rotates, the ViewModel makes sure the data is still intact.

I've created three different functions.

  • observeCounter(): will be the function which the OneUpFragment will be observing and display it's value. Since there is just a direct reference to "something" going on inside the repository, I'll just reference that directly to the countRepository.
  • addToCounter(): When the Add button is clicked in the Layout, I want the fragment to call this function.
  • subFromCounter(): Same thing as the addToCounter(), just with the Sub button instead.

To make the OneUpViewModel testable, I've didn't create an instance of the CountRepository inside the OneUpViewModel (even though it would still work in this case), but have it as parameter input. Hereby I can make a mock of the CountRepository for my test and make isolated tests for the logic of the OneUpViewModel.


Testing the ViewModel

Similar to CountRepositoryTest, I will create a class in the OneUpViewModelTest in the test package.

import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.MutableLiveData
import com.nhaarman.mockitokotlin2.mock
import dk.w4.mydemoapplication.oneup.CountRepository
import dk.w4.mydemoapplication.oneup.OneUpViewModel
import org.junit.Assert.assertEquals
import org.junit.Rule
import org.junit.Test
import org.mockito.BDDMockito.given

class OneUpViewModelTest {

    @get:Rule
    val rule = InstantTaskExecutorRule()

    val countRepository: CountRepository = mock()
    val viewModel = OneUpViewModel(countRepository)

    @Test
    fun `ViewModel has data when init()`() {
        val countData = MutableLiveData(0)
        given(countRepository.counterLiveData).willReturn(countData)

        assertEquals(0, viewModel.observeCounter().value)
    }

    @Test
    fun `ViewModel can add to counter`() {
        val countData = MutableLiveData(0)
        given(countRepository.counterLiveData).willReturn(countData)

        assertEquals(0, viewModel.observeCounter().value)

        countData.postValue(1)
        viewModel.addToCounter()
        assertEquals(1, viewModel.observeCounter().value)

        countData.postValue(2)
        viewModel.addToCounter()
        assertEquals(2, viewModel.observeCounter().value)
    }

    @Test
    fun `ViewModel can sub from counter`() {
        val countData = MutableLiveData(0)
        given(countRepository.counterLiveData).willReturn(countData)

        assertEquals(0, viewModel.observeCounter().value)

        countData.postValue(-1)
        viewModel.subFromCounter()
        assertEquals(-1, viewModel.observeCounter().value)

        countData.postValue(-2)
        viewModel.subFromCounter()
        assertEquals(-2, viewModel.observeCounter().value)
    }
}

The interesting part here is the mocking of the CountRepository and how it makes the code testable.
When I'm testing the logic of the OneUpViewModel, I don't care about what is going on inside the CountRepository. So when I wish to test the function of adding one to the counter, I will assume that the logic inside the CountRepository works as it should. Therefore I will tell that my mocked object of the CountRepository, is going to be returning the value that I expect in the given situation.

Now that I am testing a LiveData object. I can change the value of the instance I created in my test to check that the OneUpViewModel code works.


The View

The only thing the fragment class should do is observe the ViewModel, and display it in the layout.

import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProviders
import dk.w4.mydemoapplication.R
import kotlinx.android.synthetic.main.one_up_fragment.*

class OneUpFragment : Fragment() {

    companion object {
        fun newInstance() = OneUpFragment()
    }

    private lateinit var oneUpViewModel: OneUpViewModel

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.one_up_fragment, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        oneUpViewModel = ViewModelProviders.of(this,viewModelFactory {
            OneUpViewModel(CountRepository())
        }).get(OneUpViewModel::class.java)

        btnAdd.setOnClickListener { oneUpViewModel.addToCounter() }
        btnSub.setOnClickListener { oneUpViewModel.subFromCounter() }

        oneUpViewModel.observeCounter().observe(this, Observer { counter ->
            tvDisplay.text = counter.toString()
        })
    }

    private inline fun <VM : ViewModel> viewModelFactory(crossinline f: () -> VM) =
        object : ViewModelProvider.Factory {
            override fun <T : ViewModel> create(aClass: Class<T>): T = f() as T
        }
}

Well, since I've made the ViewModel take parameters, it needs to create a ViewModelProvider.Factory. It's not to complex a job in Kotlin, so I guess it's okay. That said, I would recommend using a dependency injection framework (like Koin!), since we don't have to do all this, and we can have an instance of the OneUpViewModel as a val instead of a lateinit var. So, we should close our eyes for most of the code in the Fragment.

The Fragment is our LifeCycleOwner, so I'll pass this as the input when observing on the LiveData from the ViewModel. By creating an Observer as the second input, the fragment can set the text of the TextView when the count changes.

For the button, I will simply call the functions in the OneUpViewModel and not care about the logic going on inside the object.

Tadaa! Now I have a simple application, that can increment/decrement a number with Tested MVVM code.

Testing the View

I'm not going to cover this part since it's a chapter for itself. Since there are Android specific elements (The lifecycle for once), it would require something like Robolectric. For the layout elements and UX, an Espresso test would be the way to go. But, as I said. I'm not going there now.

Final words

After working with the MVVM architecture and getting comfortable with it. I would say that it's differently enjoyable to have an architecture that is well supported by the JetPack components. It does take quite some time getting used to, depending on how much you are already the entire Android environment and how well you adjust to changes (I guess).
Anyways, I hope this will help someone out, understanding some missing part in making MVVM and testing the code.