How to Make Unit Tests for ViewModel Easier to Write and Maintain
Help yourself and others with clean code
I’m a big fan of TDD. But my TDD used to finish as soon as I started to write code for ViewModel
. I used to suffer from messes my unit tests became. The more logic I added to ViewModel
, the more messes I produced in unit tests.
Eventually, I used to end up deleting tests for ViewModel
because it was too difficult to maintain and write a new one. But without tests I used to suffer from bugs that I (or another developer) had introduced.
So why is it difficult to write unit tests for ViewModel
?
ViewModel
of a complex screen may consist of ten or more dependencies.- Every class that
ViewModel
depends on have one or more public methods thatViewModel
uses. - Some public methods may return different results that change
ViewModel
’s behaviour. - Sometimes, we have to verify a sequence of calls of a class or classes. For example, it can be states of views, one view is shown and another is hidden. Also, we might have to verify that one method of a class has been called, but another hasn’t.
- It would be great to reuse code in unit tests.
So let me narrow down the list of all of the difficulties to the following questions:
- How to handle the complexity of state verifying of
ViewModel
? - How to handle the complexity of the instantiation of
ViewModel
? - How to handle the complexity of interactions with
ViewModel
s?
But before I start answering those questions, I’d like to say a couple of things:
- This article is about how to organize tests for
ViewModel
to help you easily maintain and write new ones. - I’m going to keep examples as simple as possible. Solutions might look like overengineering, but they shine in a real project. At the end of the article, I will show snippets of tests for the project I’m working on.
Let’s begin.
Example
Let’s consider the simple load/content/error case:
- The loader state is shown when data is being fetched.
- The content state is shown if data has been loaded successfully.
- The error state is shown if data has been loaded with an error.
- Data is loaded and shown when a user clicks on the retry button.
Let’s also write unit tests like this one shown below:
Problem 1: How to Handle the Complexity of State Verifying of ViewModel?
Possible solution: I find it helpful to use Verifier. Verifier is the utility class that contains verifying logic.
So, after refactoring SomeViewModelTest
, it looks like that:
Here are some benefits of Verifier:
- It increases the readability of unit tests.
- It reduces code duplication.
- Android Studio can give hints about what can be verified. So it is harder to miss something when you write a new test.
Problem 2: How to Handle the Complexity of ViewModel Instantiation in Unit Tests?
Possible solution: I find it helpful to use ViewModelBuilder
. ViewModelBuilder
is a utility class that is responsible for configuring ViewModel
to fulfil our needs.
Mocking logic has been moved to the ViewModelBuilder
. It’s important to give descriptive names for each method. So you can spend less mental efforts reading the body of a test function.
Let’s refactor SomeViewModelTest
:
Here are the benefits of ViewModelBuilder
:
- Mocking logic can be reused in different tests.
- Increase the readability of unit tests. Instantiation of view model doesn’t produce messes.
- Android Studio can give hints about what can be mocked.
Problem 3: How to Handle Complexity of Interactions With ViewModel in Unit Tests?
Possible solution: I find it helpful to use the Cases
class. The Cases
class is a utility class that encapsulates interaction logic with ViewModel
. So it’s responsible for:
- Mocking dependencies after
ViewModel
’s instantiation. - Encapsulation of interaction logic with
ViewModel
(e.g. calling public methods for clicks or public methods that Fragment or Activity calls, etc).
Is necessary to have the Case class?
I believe it is. Interactions with ViewModel
could be quite complex. It’s often needed to call the same public methods of ViewModel
in a specific order over and over again for new tests. There could also be hot observables that emit events at a random or particular time that changes ViewModel
’s behaviour.
Example
Let’s consider the following example:
ViewModel
’s data is loaded with an error.- A user clicks on the retry button
ViewModel
’s data is loaded successfully and shown.
Let’s write the Cases
class:
And the test looks like this:
Benefits of having the Cases
class:
- It allows reusing interaction logic between tests.
- Explicit naming of methods of
Cases
class gives an explicit idea of what is happening in a test. So it increases readability. - Android Studio gives hints about what case can be used.
Examples From a Real Project
Compare the following two unit tests. The first one is written carelessly. The second one is written according to the approaches.
These are only 2 of the 42 unit tests that have been written.
If unit tests are written using the first approach, it is complicated to write a new one or change the old one because of poor readability. There are also a lot of code duplications.
The second approach eliminates all of these disadvantages.
Thanks for reading!