Goal: In our ViewModel we map some data (from an API) into a few different forms suitable for driving custom views. Test that mapping.

Example: A ‘month’ of exercise data consists of a list of days with Float ‘duration’ and a String ‘description’. One View, a piece of UI that displays a “✔” or an “❌” might be driven by a list of booleans. Another, a TextView displaying duration might be driven by a list of Strings. We use the ViewModel to do that mapping from “raw” data into View data. There may be some more or less complex business logic involved in that mapping.

First question: How do we assert that a LiveData has emitted a value.

See the article “Unit-testing LiveData and other common observability problems” on the Official Android Developers Medium channel.

In short – we will use a Google provided extension:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }

    this.observeForever(observer)

    // Don't wait indefinitely if the LiveData is not set.
    if (!latch.await(time, timeUnit)) {
        throw TimeoutException("LiveData value was never set.")
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

And we will use it like this:

1
2
3
4
5
6
7
8
9
10
/* Copyright 2019 Google LLC.	
   SPDX-License-Identifier: Apache-2.0 */
@Test
fun isLiveDataEmitting_getOrAwaitValue() {
    viewModel.setNewValue("foo")

    // Pass:
    assertEquals(viewModel.liveData1.getOrAwaitValue(), "foo")
    assertEquals(viewModel.liveData2.getOrAwaitValue(), "FOO")
}

Second Question: Our ViewModel gets a Repository class that provides our “raw” stream of data, how do we mock that out? — Like this:

1
2
3
4
/// all of our Repository implementations conform to this interface.
interface IRepository {
     fun meditationData(): MutableLiveData<List<Pair<Float, String>>>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// This is the mocked implementation
class MockRepo: IRepository {
    private val _meditationData = MutableLiveData<List<Pair<Float, String>>>()
    override fun meditationData(): MutableLiveData<List<Pair<Float, String>>> {
       return _meditationData
    }
}

// So in our test, we mock one out.
class AchievmentViewModelTest {
    private val mockRepo = MockRepo()
    private val model = AchievmentViewModel(mockRepo)
    private val testData = listOf(Pair(0f, ""), Pair(15f, ""), Pair(0f, ""))

    // some boilerplate needed here, see below

    @Before
    fun before() {
        mockRepo.meditationData().value = testData
    }

What does a test look like?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
fun getLastFourWeeksBackingModel() {

    val resultExpected = ...

    // get value from the LD
    val result = model.lastFourWeeksBackingModel.getOrAwaitValue()

    // simple check
    assertEquals(result.size, resultExpected().size)

    //  iterative check of ViewState
    val err = "Failure index %d %s != %s"
    result.zip(resultExpected()).forEachIndexed{index, p ->
        assertTrue(
            String.format(err, index, p.first::class, p.second::class),
            /* condition */)
    }

Finally, what boilerplate do we need, other than the Extension mentioned above?

// Required to get testing internals to work.
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule()

Putting it all together, we now have a way to test the business logic that maps from our Repo to our Views through a ViewModel.