Testing ViewModel in MVVM using LiveData and RxJava

Nicolas Duponchel
3 min readMay 23, 2018

--

In my team, we tried out the MVVM architecture with android.arch.lifecycle from Google. No doubt it’s amazing. This article points out how to write ViewModel unit tests.

  • Gradle libraries
  • Testing LiveData values
  • Mocking RxJava with trampoline

Gradle

I use mockito and google truth.

testImplementation "junit:junit:4.12"
testImplementation "com.google.truth:truth:0.36"
testImplementation "org.mockito:mockito-core:2.13.0"
testImplementation "android.arch.core:core-testing:1.1.1"

Testing LiveData values

The right way to test LiveData is to store their values in a particular list, then look over this list and check if the content is correct.

First of all, let’s create our custom Observer to populate the list.

class TestObserver<T> : Observer<T> {

val observedValues = mutableListOf<T?>()

override fun onChanged(value: T?) {
observedValues.add(value)
}
}

Then, the extension function to use it.

fun <T> LiveData<T>.testObserver() = TestObserver<T>().also {
observeForever(it)
}

A specific TestRule is required for testing architecture components : InstantTaskExecutorRule. Finally here is the test.

@get:Rule
val mockitoRule = MockitoJUnit.rule()

@get:Rule
val taskExecutorRule = InstantTaskExecutorRule()

@InjectMocks
lateinit var classUnderTest: YourViewModel
@Test
fun `init method sets liveData value to empty list`() {
val liveDataUnderTest = classUnderTest.liveData.testObserver()

Truth.assert_()
.that(liveDataUnderTest.observedValues)
.isEqualTo(listOf(emptyList<String>()))
}

Cool isn’t it ? But why do we need a List to store nothing ? Let’s go deeper and have a look at the following example. It is an installation and the screen takes 3 states.

@Test
fun `onInstall set correct screen states`() {
val liveDataUnderTest = classUnderTest.screenState.testObserver()
classUnderTest.onInstall() Truth.assert_()
.that(liveDataUnderTest.observedValues)
.isEqualTo(listOf(INITIAL, LOADING, INSTALLED))
}

Testing Rx methods

Now that we’ve done with LiveData values, let’s have a look at Rx. If we try to run the previous test with onInstall() which use Rx, we face to this error :

java.lang.AssertionError: Not true that <[INITIAL]> is equal to <[INITIAL, LOADING, INSTALLED]>

The task is executed on parallel with io Scheduler. That’s why the test execution is faster than the operation on background. The values LOADING and INSTALLED have no time to be set before the test ends.

Rx Trampoline

What’s beautiful with RxJava is that everything your may need already exists. To achieve unit testing, there’s a specific scheduler called Trampoline. All jobs that subscribes to it will be queued and excuted one by one. Let’s create a new TestRule to use it.

class RxSchedulerRule : TestRule {

override fun apply(base: Statement, description: Description) =
object : Statement() {
override fun evaluate() {
RxAndroidPlugins.reset()
RxAndroidPlugins.setInitMainThreadSchedulerHandler { SCHEDULER_INSTANCE }

RxJavaPlugins.reset()
RxJavaPlugins.setIoSchedulerHandler { SCHEDULER_INSTANCE }
RxJavaPlugins.setNewThreadSchedulerHandler { SCHEDULER_INSTANCE }
RxJavaPlugins.setComputationSchedulerHandler { SCHEDULER_INSTANCE }

base.evaluate()
}
}

companion object {
private val SCHEDULER_INSTANCE = Schedulers.trampoline()
}
}

Just need to add this TestRule to the test.

@get:Rule
val rxSchedulerRule = RxSchedulerRule()

Process finished with exit code 0

For those who would be more curious, I wrote this article using a previous MVVM project on which I added some unit tests. You can find it on GitHub here.

--

--