Testing ViewModels: The Journey
Testing ViewModels and LiveData based logic synchronously and sequentially can bring some hidden consequences; on some occasions, it will be desirable to check if the steps the system takes to reach a specific state are correct.
Long story
Those times where asynchronous code was a nightmare for every Android developer seems to be very far away. Coroutines now offer a great tool to manage async code, and Google is working hard to improve the Android tooling.
But great power comes with great responsibilities, and we need to worry about testing our asynchronous code more than ever.
An example of asynchronous code
Google recommends using LiveData, ViewModels, and Coroutines to architect our apps. LiveData offers a convenient bridge to send information to our UI, ViewModel helps us to persist our data and separate concerns between business logic and presentation logic, and Coroutines are a lightweight tool to handle asynchronous code in a sequential programming style.
Let’s take a look at a possible example of this through an app that requests information from the network and presents it to the user.
We define three states for our data:
- Success: indicating that the network request ended and the data is available to be consumed.
- Loading: indicating that we are executing the network request, and the information is not yet ready to use.
- Error: indicating that something went wrong.
The ViewModel will decide, keep and push the current state to the UI using the LiveData and, for the sake of simplicity, handle the network request.
So when the app starts the UI controller (Activity) asks the ViewModel to provide some data, the ViewModel sends through the LiveData a Loading state to the UI to display a loading indicator, and it launches a coroutine to perform an asynchronous network call to get the requested data by the UI. After some time, once the data is downloaded and ready, the ViewModel pushes a Success state to remove the loading indicator and present the information, or if something went wrong, an Error state.
Asynchronous code
We use a background process to execute the network call to prevent the UI thread from blocking, and keep the app responsive to the user interaction; to do this, we perform the request inside a coroutine bound to the ViewModel’s lifecycle using viewModelScope.
The problem
If we want the coroutine to run on a thread different than the UI thread, we need to specify a Dispatcher that our coroutine will use, to do so, we have two alternatives:
- Define right away the dispatcher when we launch the coroutine. In this case, the dispatcher should be injected into the class (this will help to write tests for the code; we will talk about that in a minute).
- Or, we can use the dispatcher that the VideModel defines by default and rely on a suspending function that handles the dispatcher.
In the first case (and in the second depending on which tools we use), our tests could be unreliable for the following reason:
- If we execute code that contains an asynchronous operation in a test, then the system will use a different thread other than the test thread to execute the coroutine.
- At this point, the coroutine and the test will be running in two different threads. The test thread won’t wait or suspend until the coroutine ends and returns the information, and it will reach the verification step too soon. In the best scenario, the data won’t be ready sometimes, failing, and making out test non-deterministic or flaky.
This is why we need to find a way to run our code inside a test sequentially in the same thread, this guarantees that the test result will always be the same.
So paradoxically, we ended up needing to turn asynchronous code into synchronous for testing.
Building synchronous tests
We got a couple of tools that will help us running synchronously asynchronous code.
- TestCoroutineDispatcher: this is a particular dispatcher that will execute scheduled actions immediately, one great thing of this dispatcher is that it runs using virtual time that we can pause and modify to inspect the state of execution in different points of time.
- InstantTaskExecutorRule: JUnit test rule that swaps the background executor used by the Architecture Components with a different one that executes everything synchronously.
If we combine both tools, we can achieve synchronous tests that execute deterministically, check this excellent talk to understand how it works, but in a few words:
We can use the TestCoroutineDispatcher to run our tests in a single thread and inject it into our ViewModels to guarantee that our asynchronous code runs in the same thread that the test does.
Or, if we decided to use the scope that the ViewModel provides to launch our coroutines, then we should use the InstantTaskExecutionRule to run any coroutine launched in the ViewModel in a Dispatcher defined by us, in this case, the TestCoroutineDispatcher.
Following any of the two alternatives, or combining both, our tests and methods will run synchronously in the same thread, which will always return the same result.
The hidden issue
With the architecture before described, we mainly want to test if our ViewModel computes and sends the right state through the LiveData:
- When the app starts, the LiveData should contain a Loading state.
- When the data download ends, the LiveData should provide a Success state with the data.
- When something fails, the LiveData should return an Error state.
Ultimately, we are checking snapshots of the LiveData state during the execution of the test. This brings a hidden issue: what if in between two states an unexpected value is pushed? As LiveData only returns the last value, we wouldn’t detect this problem.
There is an easy solution for this; we can record all events received through the LiveData:
This way, we will have a stored LiveData timeline where we can check if the stream of events received on it is the one that we expect, or if on the contrary, something sneaked into the LiveData causing glitches or unknown issues.
Conclusion
Designing good tests is hard, luckily we can always infer some patterns to test those parts of our system that repeats consistently like presenters, controller, or ViewModels.
In this post, we have seen three tools: TestCoroutineDispatcher, InstantTaskExecutionRule, and LiveData observer pattern that we could use to build the basic structure for our ViewModel tests.
Happy testing!
Thanks to Cesar Valiente and Antonio C for the feedback on this article.
Thanks to Vipul Shah for finding an error in the observeForTesting extension ;).