Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing LiveData Transformations?

I've built a Splash Screen using Android Architecture Components and Reactive approach. I return from Preferences LiveData object fun isFirstLaunchLD(): SharedPreferencesLiveData<Boolean>. I have ViewModel that passes LiveData to the view and updates Preferences

val isFirstLaunch = Transformations.map(preferences.isFirstLaunchLD()) { isFirstLaunch ->
    if (isFirstLaunch) {
        preferences.isFirstLaunch = false
    }
    isFirstLaunch
}

In my Fragment, I observe LiveData from ViewModel

    viewModel.isFirstLaunch.observe(this, Observer { isFirstLaunch ->
        if (isFirstLaunch) {
            animationView.playAnimation()
        } else {
            navigateNext()
        }
    })

I would like to test my ViewModel now to see if isFirstLaunch is updated properly. How can I test it? Have I separated all layers correctly? What kind of tests would you write on this sample code?

like image 531
qbait Avatar asked Aug 12 '18 15:08

qbait


People also ask

What is transformation in LiveData?

Transformations for a LiveData class. You can use transformation methods to carry information across the observer's lifecycle. The transformations aren't calculated unless an observer is observing the returned LiveData object.

How do you observe LiveData in unit testing?

In order to test LiveData, we need to take care of two things. First, the LiveData should be observed in order to work properly. You can find a utility class at the end of the section to observe your LiveData from test class. Secondly we should add InstantExecutorRule test rule.

When transformation map () method is executed on live data What will it return?

map() will return a LiveData object that should be observed for the mapFunction to be called. Assume a MutableLiveData Int variable that holds a handicap increment value. When this value changes, avgWHDCP for all bowlers in the list needs to be re-computed. Initially it is set to zero.

Can LiveData have multiple observers?

LiveData is amazing android architectural component for most of use cases where we need to fetch data from some data source.


1 Answers

Have I separated all layers correctly?

The layers seem reasonably separated. The logic is in the ViewModel and you're not referring to storing Android Views/Fragments/Activities in the ViewModel.

What kind of tests would you write on this sample code?

When testing your ViewModel you can write instrumentation or pure unit tests on this code. For unit testing, you might need to figure out how to make a test double for preferences, so that you can focus on the isFirstLaunch/map behavior. An easy way to do that is passing a fake preference test double into the ViewModel.

How can I test it?

I wrote a little blurb on testing LiveData Transformations, read on!

Testing LiveData Transformations

Tl;DR You can test LiveData transformation, you just need to make sure the result LiveData of the Transformation is observed.

Fact 1: LiveData doesn't emit data if it's not observed. LiveData's "lifecycle awareness" is all about avoiding extra work. LiveData knows what lifecycle state it's observers (usually Activities/Fragments) are in. This allows LiveData to know if it's being observed by anything actually on-screen. If LiveData aren't observed or if their observers are off-screen, the observers are not triggered (an observer's onChanged method isn't called). This is useful because it keeps you from doing extra work "updating/displaying" an off-screen Fragment, for example.

Fact 2: LiveData generated by Transformations must be observed for the transformation to trigger. For Transformation to be triggered, the result LiveData (in this case, isFirstLaunch) must be observed. Again, without observation, the LiveData observers aren't triggered, and neither are the transformations.

When you're unit testing a ViewModel, you shouldn't have or need access to a Fragment/Activity. If you can't set up an observer the normal way, how do you unit test?

Fact 3: In your tests, you don't need a LifecycleOwner to observe LiveData, you can use observeForever You do not need a lifecycle observer to be able to test LiveData. This is confusing because generally outside of tests (ie in your production code), you'll use a LifecycleObserver like an Activity or Fragment.

In tests you can use the LiveData method observeForever() to observer without a lifecycle owner. This observer is "always" observing and doesn't have a concept of on/off screen since there's no LifecycleOwner. You must therefore manually remove the observer using removeObserver(observer).

Putting this all together, you can use observeForever to test your Transformations code:

class ViewModelTest {

    // Executes each task synchronously using Architecture Components.
    // For tests and required for LiveData to function deterministically!
    @get:Rule
    val rule = InstantTaskExecutorRule()


    @Test
    fun isFirstLaunchTest() {

        // Create observer - no need for it to do anything!
        val observer = Observer<Boolean> {}

        try {
            // Sets up the state you're testing for in the VM
            // This affects the INPUT LiveData of the transformation
            viewModel.someMethodThatAffectsFirstLaunchLiveData()

            // Observe the OUTPUT LiveData forever
            // Even though the observer itself doesn't do anything
            // it ensures any map functions needed to calculate
            // isFirstLaunch will be run.
            viewModel.isFirstLaunch.observeForever(observer)

            assertEquals(viewModel.isFirstLaunch.value, true)
        } finally {
            // Whatever happens, don't forget to remove the observer!
            viewModel.isFirstLaunch.removeObserver(observer)
        }
    }

}

A few notes:

  • You need to use InstantTaskExecutorRule() to get your LiveData updates to execute synchronously. You'll need the androidx.arch.core:core-testing:<current-version> to use this rule.
  • While you'll often see observeForever in test code, it also sometimes makes its way into production code. Just keep in mind that when you're using observeForever in production code, you lose the benefits of lifecycle awareness. You must also make sure not to forget to remove the observer!

Finally, if you're writing a lot of these tests, the try, observe-catch-remove-code can get tedious. If you're using Kotlin, you can make an extension function that will simplify the code and avoid the possibility of forgetting to remove the observer. There are two options:

Option 1

/**
 * Observes a [LiveData] until the `block` is done executing.
 */
fun <T> LiveData<T>.observeForTesting(block: () -> Unit) {
    val observer = Observer<T> { }
    try {
        observeForever(observer)
        block()
    } finally {
        removeObserver(observer)
    }
}

Which would make the test look like:

class ViewModelTest {

    @get:Rule
    val rule = InstantTaskExecutorRule()


    @Test
    fun isFirstLaunchTest() {

        viewModel.someMethodThatAffectsFirstLaunchLiveData()

        // observeForTesting using the OUTPUT livedata
        viewModel.isFirstLaunch.observeForTesting {

            assertEquals(viewModel.isFirstLaunch.value, true)

        }
    }

}

Option 2

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            [email protected](this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

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

    } finally {
        this.removeObserver(observer)
    }

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

Which would make the test look like:

class ViewModelTest {

    @get:Rule
    val rule = InstantTaskExecutorRule()

    @Test
    fun isFirstLaunchTest() {

        viewModel.someMethodThatAffectsFirstLaunchLiveData()

        // getOrAwaitValue using the OUTPUT livedata        
        assertEquals(viewModel.isFirstLaunch.getOrAwaitValue(), true)

    }
}

These options were both taken from the reactive branch of Architecture Blueprints.

like image 188
Lyla Avatar answered Sep 18 '22 15:09

Lyla