How to properly test user interfaces in Android using Hilt and ViewModel

 

Marko Lalic, Android Developer

Intro

Testing user interfaces (UI) is a crucial aspect of software development, as it ensures the quality, stability, and overall user experience of an application. One effective approach to UI testing is to mock the ViewModel, which allows for a high degree of isolation and control over the tested components. This method focuses on verifying the UI’s behavior and its adaptation to different states, while removing dependencies on the actual backend implementation.

By mocking the ViewModel, UI tests can be more targeted and reliable, as they simulate various ViewModel states and responses without the need for a live backend connection. This approach eliminates potential sources of unexpected errors and inconsistencies that might arise from backend issues or network fluctuations. Consequently, UI tests can be executed faster and in a more controlled environment, leading to more efficient development cycles and improved application quality.

In contrast, end-to-end (E2E) tests involve the entire application stack, including the backend implementation. While E2E tests are essential for validating the overall functionality and integration of components, they can introduce complexities and uncertainties that make isolating and identifying issues more challenging. Moreover, E2E tests tend to be slower and more resource-intensive, which can impact development velocity.

TL/DR

 

By injecting ViewModelProvider.Factory in our UI components: Activities and Fragments, we can provide different implementation of ViewModel for production and for tests. This is easily achievable with using Hilt DI which can inject ViewModelProvider.Factory in ActivityRetainedComponent (both Activity and Fragment). Also, by using @TestInstallIn we can easily change and mock ViewModelProvider.Factory and provide fake ViewModel implementation in androidTest setup.

ViewModel setup

 

First, create Abstract ViewModel in order to encapsulate data and expose api to UI components (Activities and Fragments).

Abstract encapsulation of Custom ViewModel

Create default implementation for CustomViewModel. In this example, we have some depenencies constructor injected. We will use ViewModelProvider.Factory in order to inject them later on to our ViewModel.

Default ViewModel implementation

Create a Hilt Module which provides ViewModel.Factory for CustomViewModel.

Dependencies dependency1 and dependency2 are provided by other Hilt modules…

Also, we are adding a custom @Qualifier in order to know which ViewModelProvider.Factory we are injecting in our Fragments or Activities.

Provide CustomViewModelFactory in Hilt DI

Fragment setup

Fragment should be annotated with AndroidEntryPoint annotation in which we are injecting our ViewModelProvider.Factory.

Notice here that if ViewModel is scoped to activity, then factory should be injected in Activity, and not in Fragment (it is not necessary as it will reuse the already created viewmodel by parend activity).

Android Test setup

Finally, the part for which we created this layers of abstraction in the first place.

First things first, we are using Hilt and we want to test our UI components in isolation (Fragments).

It is not possible to use launchFragmentInContainer from the androidx.fragment:fragment-testing library with Hilt, because it relies on an activity that is not annotated with @AndroidEntryPoint.

In order to bypass this limitation, we must write our own launchFragmentInHiltContainer extension in order to launch our Fragments in AndroidEntryPoint annotated empty Activity.

launchFragmentInHiltContainer method workaround

Then create HiltTestActivity class.

For activity-scoped-viewmodels, you can add factory in both Activity and Fragment, it has no side effects.

This could potentially lead to inconsistencies if the custom factory used in the Fragment is different from the one used in the Activity, so be careful and use the right annotation!

I prefer to leave by activityViewModels() empty in my Fragments and use Factory in my Activities for activity-scoped-viewmodels.

However, this approach requires to inject ViewModel Factory in HiltTestActivity in order to be able to run tests in isolation for Fragments and to properly instantiate ViewModel.

If you, like me, like your by activityViewModels() empty in your Fragments, this is necessary workaround:

  • inject custom view model factory
  • in onCreate of activity, call toString() or any other method without side effect, because the initalisation is lazy and requires to actually use viewmodel in order to instantiate it.
HiltTestActivity

Now it’s time to replace ViewModel Factory with a Fake version which will instantiate FakeViewModel. This file should be located in the same folder in which androidTest classes are located.

You can read more about replacing Hilt bindings in tests here.

Here, we are overriding generateText fun, you can override any method of superclass which we defined at the start of this journey, as they are the only way of communicating with our UI.

Fake CustomViewModel Factory 

Finally, this is what your tests will look like in the end.

Notice that you can cache CustomViewModel in FakeCustomViewModelProviderModule object in order to manipulate with the data and test how your UI reacts.

You could also inject different variations of FakeCustomViewModel in different test classes, the freedom is on you to decide how you want to facilitate your testing.

 

Happy testing!

I am glad if this article has bought you some time and helped you out with your Android tests. Don’t forget to add your tests to your CI mechanism of choice, and to deploy when tests are passing! 😃 

Sign In