An Android app I’m currently working on has the following architecture:
When it came to testing the views , I wanted to write some UI tests using Espresso. These would just test just the activities and fragments with a mock backend.
With the Architecture Components I thought this would be fairly simple since all the interactions between the view and the backend services should be done through the ViewModel. Hence all I would need to do is to provide a mock ViewModel (in my case using Mockito).
Additionally I wanted to find a solution that:
- was simple and straightforward, without the need for workarounds or hacks if possible
- did not require any changes or additional code in the app just to accommodate testing
Looking at the Google sample code – no Dagger for UI Tests!
Since Google provides sample code for the Architecture Components, that’s the first place I looked. In particular the GithubBrowserSample seemed to be what I was after, this is from the README:
UI Tests
The projects uses Espresso for UI testing. Since each fragment is limited to a ViewModel, each test mocks related ViewModel to run the tests.
However, when I looked at the sample code for their UI tests, I was in for a surprise. Although the sample app itself uses Dagger DI, the UI tests do not.
This differs from most examples of testing Dagger applications, where it is advocated to write additional Components and Modules to inject fake dependencies for testing.
How it is done? The Setup.
Since I couldn’t find any documentation for these UI tests , here is a quick summary of how the GithubBrowserSample apps mocks the ViewModel without using Dagger. This basically involves using test version of the application and activity classes which do not invoke the Dagger injection code.
ViewModel Factory
Although it is possible to inject a ViewModel into an activity or fragment with Dagger, I will be using a custom ViewModel factory and injecting that instead. This is done for the following reasons:
- Injecting a ViewModel class is only possible for ViewModels that have a default (empty) constructor. If the ViewModel constructor has parameters, then you need to inject a factory class that implements ViewModelProvider.Factory.
- It seems to be a common pattern when using Dagger with the ViewModel to create a Module to encapsulate the ViewModel injection code. This Module would bind the ViewModel classes used in the app into a map. It would also provide the ViewModel factory class, which in turn uses the map to create the ViewModel classes. This is the pattern used in the GithubBrowserSample.
Here is a brief description of the UI tests for the fragments in the sample code:
- Create a test application class.
This is just a dummy application class that does not invoke the Dagger code that builds the object graph.
https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/TestApp.java
- Create a test activity class.
This is just a dummy activity class to contain the fragment to be tested. It allows the fragment to be inserted into it.
https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/debug/java/com/android/example/github/testing/SingleFragmentActivity.java
This test runner will used the test application class instead of the application class for the app.
https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/util/GithubTestRunner.java
@Override
public Application newApplication(ClassLoader cl, String className, Context context)
throws InstantiationException, IllegalAccessException, ClassNotFoundException {
return super.newApplication(cl, TestApp.class.getName(), context);
}
android {
.
.
.
testInstrumentationRunner "com.android.example.github.util.GithubTestRunner"
}
How it is done? The UI Test.
The GithubBrowserSample has several UI tests for different fragments, but they follow the same basic pattern. Let’s use this one as an example:
https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSample/app/src/androidTest/java/com/android/example/github/ui/user/UserFragmentTest.java
Remember that when the UI tests are run, there is no Dagger dependency injection.
- Use the test activity class to hold the fragment to be tested, instead of the activity used in the app. This is done in the ActivityTestRule used in setting up Espresso tests.
@Rule
public ActivityTestRule<SingleFragmentActivity> activityRule =
new ActivityTestRule<>(SingleFragmentActivity.class, true, true);
- Before the test is run, setup the mock ViewModel.
@Before
public void init() {
.
.
viewModel = mock(UserViewModel.class);
when(viewModel.getUser()).thenReturn(userData);
when(viewModel.getRepositories()).thenReturn(repoListData);
.
.
}
- Set the ViewModel factory field in the fragment (which would have been injected in the app) to a fake factory class that just passes the mocked ViewModel. Of course the field needs to be accessible from the test class for this to happen (i.e. public or default package access).
@Before
public void init() {
.
.
fragment.viewModelFactory = ViewModelUtil.createFor(viewModel);
.
.
}
- Put the fragment into the test activity.
@Before
public void init() {
.
.
.
activityRule.getActivity().setFragment(fragment);
}
- Because this fragment uses LiveData to get data from the ViewModel, the test data is inserted into the LiveData returned from the mock ViewModel.
@Test
public void loadingWithUser() {
User user = TestUtil.createUser("foo");
userData.postValue(Resource.loading(user));
.
.
}
A simple approach
That’s it.
With this approach there is no need to worry about the Dagger configuration. Just mock the ViewModel to return the data you want for the Espresso test.
Disadvantages
- Using a custom test runner and test application means that all of instrumentation tests in the androidTest directory will have Dagger disabled.
But what if you have other instrumentation tests where you do want to use the Dagger injected classes, in additition to the UI tests. You don’t have the flexibility of deciding whether to enable / disable the Dagger DI on a test by test basis.
2. If you are using the Dagger Android library, then this approach will work with fragments, but not with activities. This is because an AndroidInjector is used in the onCreate() method of the activity to inject the dependencies.
@Override
public void onCreate (Bundle savedInstanceState) {
AndroidInjection.inject(this);
super.onCreate(savedInstanceState);
.
.
}
In the next post, I will explore some of the options if we want to mock the ViewModel in the UI tests with Dagger.