Unit-testing LiveData and other common observability problems
Next time you’re scratching your head wondering why an innocent looking unit test with LiveDatas is failing, or staring at an empty screen that should show… something, you should remember that a LiveData that is not observed won’t emit updates. In this post you’ll learn some good practices to avoid this problem.
This is especially important when dealing with Transformations. If a LiveData is transformed, the result of the transformation must be observed. Otherwise the transformation will never be evaluated.
If a LiveData falls in a forest and no one is observing it… was it ever updated?
In this example, the value of the initial _liveData1
is transformed (mapped) to upper case whenever the initial value changes:
Let’s write a simple test for it:
This test fails because LiveData doesn’t do more work than needed. Reading liveData2.value
doesn’t initiate the chain of dependent transformations because it’s not observed. Only a subscription through observe()
does that.
Note that liveData1
’s value can be read because it’s a MutableLiveData
and no transformations need to be evaluated.
A simple (but not great) way to fix this issue would be to call observeForever()
on the LiveDatas that we need to read:
However if we need to observe multiple LiveDatas this can fill our tests quickly with unimportant statements.
To fix this, Jetpack doesn’t provide a test helper yet, but we can make our own:
Also available in Java.
This function observes a LiveData until it receives a new value (via onChanged
) and then it removes the observer. If the LiveData already has a value, it returns it immediately. Additionally, if the value is never set, it will throw an exception after 2 seconds (or whatever you set). This prevents tests that never finish when something goes wrong.
Alternatively, if you need to keep observing a LiveData throughout a test, because it might receive multiple values that you want to check, take a look at
observeForTesting
in the LiveData sample.
Now the test is much more readable:
Note: While you don’t need to do this with MutableLiveDatas, it’s good practice to getOrAwaitValue
when reading any LiveData value in case the implementation detail changes in the future:
InstantTaskExecutorRule
It’s important to note that this technique can have threading issues. Most of them can be solved by adding InstantTaskExecutorRule to your unit tests. However, if you call LiveData.postValue()
from the main thread, the documented precedence might not be preserved. This is not common but worth mentioning because it can create evasive bugs.
See it in action in this test in the LiveData sample.
Other scenarios
Wrong ViewModel
If you’re using a shared ViewModel between multiple fragments, make sure you’re using the same instance in all screens. This can happen when passing the Fragment instead of the Activity as the LifecycleOwner to the ViewModelProviders or using by ViewModels
in a fragment instead of by activityViewModels()
.
Wrong Room database instance
Why is a query that returns a LiveData not emitting any updates?
If you’re sure that the LiveData is being observed, you might be using different instances of the database. Check your creation patterns or your DI graph. If you know what you’re doing, you can also enable multi-instance invalidation in the database builder, but it has some limitations.
You can see LiveDataTestUtil in action in the LiveDataSample.
What other scenarios have you found? Let us know in the comments!