Effective LiveData and ViewModel Testing
Many has been written about Architecture Components and how to implement them, having one of the benefits improved testability. Ok, but how does it actually improve? Let’s have a look.
Architecture Components were introduced on Google I/O 2017 and they meant significant improvement in Android development world. Best practices learned over years were transformed into libraries handling most common problems and opinionated guide was out.
View and ViewModel communicates via LiveData
One of the key thoughts of Architecture Components is Observer Pattern for updating Activity or Fragment to handle commonly experienced problems.
Important part here is View remains passive, keeping all the logic in ViewModel, also dispatching user actions immediately to ViewModel. Complexity to verify by test should remain in ViewModel and if possible, we should avoid android.*
dependencies to be able to test with pure JUnit test. This is important to achieve Effective ViewModel Test.
Effective ViewModel Test
We could discuss for ages, which test is effective and worth to write, but lets now set some goals we want our tests to achieve:
- Very fast test execution
- Modelling real use case of ViewModel under test
- Test is easy to write and maintain
Resolving first requirement means keeping the test in JUnit only, without using Robolectric or instrumentation test. This is possible to achieve and potentially worth the effort.
Discussing requirement 2. brings us to the point why following Architecture Components makes sense in case of improved testing. Separation of components makes easy to replace Activity by something else. Test can behave as Activity whilst keeping the test modelling real use case of interaction with ViewModel.
Now let’s discuss the last requirement of easy writing and maintenance.
Writing effective ViewModel test
During the first presentation of LiveData and ViewModel components, expected question was soon raised: “Is this RxJava?”. It is not, but there are clearly some common patterns.
Having this in mind, together with the fact that RxJava is brilliant and very well tested library, we can take inspiration from testing approach there. Each type has a method test()
returning TestObserver
or TestSubscriber
to perform fluent test assertions.
We can implement our own TestObserver
for LiveData
with required assert methods and since we now wield tools like Kotlin extension methods, we can achieve same comfort as RxJava to have test()
method on LiveData
directly. Code using livedata-testing library will look like this:
viewModel.counterLiveData()
.test()
.assertHasValue()
.assertValue { it > 3 }
.assertValue(4)
.assertNever { it > 4 }viewModel.plusButtonClicked() // internally increments counterviewModel.counterLiveData()
.test()
.assertValue(5)
.assertNever { it > 5}
...
Implementation of this uses simple extension method to subscribe to LiveData
and return TestObserver
having introduced assertion methods.
fun <T> LiveData<T>.test(): TestObserver<T> {
return TestObserver.test(this)
}
TestObserver
is Java class, so you can use this approach even if your project is using only Java without Kotlin. The only difference would be calling static method:
TestObserver.test(viewModel.counterLiveData())
.assertHasValue()
...;
Please keep in mind that all tests using LiveData
must currently include InstantTaskExecutorRule
from android.arch.core:core-testing
artifact.
Is writing tests like this effective?
So far, writing tests like this fulfilled the goals set:
- Test is executed fast by using pure JUnit test.
- Test is modelling Activity, therefore the use case is the same as in production Android code.
3. Using TestObserver
and possibly Kotlin extension methods in our project and properly implementing the Architecture Components ideas, our test should be fast to write and potentially easy to maintain.
Checking livedata-testing library might give you more ideas and examples how to write tests like this. If you decide to try it or if you use different testing approach, let me know what you think. Happy testing!