Android Testing with Dagger and Mockito

The testing phase of software development is often overlooked and undervalued in an effort to release a feature as fast as possible. Bugs, both fatal and non-fatal, detract from the user experience and can ruin an otherwise “killer” feature. Having the proper test tools is key for better coverage in unit, functional, and integration cases. At Yodle we use a mix of JUnit, Robolectric, and Espresso to test everything from views to entire workflows. Our apps use Dagger 2 for dependency injection, and we wanted to share an example of all these tools coming together.

To follow along, check out our library and sample app on Github.

Our business logic is broken up into various service classes that can be wired into other components using the standard @Inject annotation. When defining modules, we can tell Dagger to return Mockito versions of our dependencies. However, having mocks everywhere can lead to massive setup blocks in test cases, at least in our case. To alleviate this, we made our own annotation named@RealClass which returns a real instance of the class. This annotation and processor can be found in our library Bayonet (Get it? Dagger? Bayonet? Take a moment to enjoy that before you continue reading).

There are three main parts to our Dagger setup:

  • MainApp — The class that extends Application in your Android app
  • AppComponent — Your Dagger component class ( @Component )
  • AppModule — Your Dagger module class ( @Module )

There is a BaseAppModule that all our *AppModules extend, which returns instances of classes that are inject-able in our app. The normal AppModule will simply return the base method’s implementation, and the Robolectric and Espresso versions will use a special method called “provide” that returns a mock or real version based on the presence of @RealClass for an injected field. This adds up to a powerful way to control your injected dependencies and ensure certain behaviors in everything from activities to custom views.

We use @RealClass injections to allow classes to do their thing without the need for huge amounts of mocking in a setup. Typically we always mock Analytics or things that make external network calls, unless it is an integration test.

When testing activities, we want to mock the behavior of injected dependencies in order to test the various states the activity may have. For these types of tests, we inject dependencies of the activity using just @Inject so their behavior can be mocked and verified. The following snippet can be found in MainActivityTest.java

@Inject UserService userService;
@Test
public void givenUsernameAndPassword_whenLogin_callUserService() {
fillFormAndSubmit(username, password);

verify(userService).login(username, password);
}

In the following example, we will look at the scenario where an injected dependency is the class under test. In these cases, we use @RealClass to get an instance of the class under test, then inject any dependencies it may have so the internal behavior can be mocked and verified. The ApiClient in the following test is really a dependency inside UserService . By mocking the internal dependency, we can freely call methods on our @RealClass . This snippet can be found in UserServiceTest.java

@RealClass @Inject UserService userService; // real class under test
@Inject ApiClient apiClient; // mock
@Test
public void givenAdminPermission_whenLogin_returnTrue() {
when(apiClient.authenticate(username, password))
.thenReturn(Lists.newArrayList("ADMIN"));
    assertTrue(userService.login(username, password));
}

Sometimes we want a mix of both real and mock injections, like in Espresso tests. Like in the previous scenario, we mock ApiClient but use a real version of UserService so stepping through the app with Espresso exercises all code up to the network layer. If we wanted to turn this into an integration test, we can simply add@RealClass to ApiClient and remove the mocking code for it. The following snippet can be found in LoginTest.java

@RealClass @Inject UserService userService;
@Inject ApiClient apiClient;
@Test
public void testLoginSuccess() {
when(apiClient.authenticate(username, password))
.thenReturn(Lists.newArrayList("ADMIN"));

onView(withId(R.id.username)).perform(typeText(username));
onView(withId(R.id.password)).perform(typeText(password));
onView(withId(R.id.login)).perform(click());

onView(withId(R.id.login_message))
.check(matches(isDisplayed()));
onView(withId(R.id.login_message))
.check(matches(withText(R.string.login_success)));
}

We hope to roll out improvements and additions to this framework in the future.

Feel free to drop a line to let us know what you think!