Testing with Dagger

One of the benefits of using dependency injection frameworks like Dagger is that it makes testing your code easier. This document explores some strategies for testing applications built with Dagger.

Replace bindings for functional/integration/end-to-end testing

Functional/integration/end-to-end tests typically use the production application, but substitute fakes (don’t use mocks in large functional tests!) for persistence, backends, and auth systems, leaving the rest of the application to operate normally. That approach lends itself to having one (or maybe a small finite number) of test configurations, where the test configuration replaces some of the bindings in the prod configuration.

Separate component configurations

The recommended approach is to have a separate component configuration for each environment (e.g. production and testing). The testing component type extends the production component type so that it gets all of the same entry point and corresponding interfaces, but it installs a different set of modules.

@Component(modules = {
  OAuthModule.class, // real auth
  FooServiceModule.class, // real backend
  OtherApplicationModule.class,
  /* … */ })
interface ProductionComponent {
  Server server();
}

@Component(modules = {
  FakeAuthModule.class, // fake auth
  FakeFooServiceModule.class, // fake backend
  OtherApplicationModule.class,
  /* … */})
interface TestComponent extends ProductionComponent {
  FakeAuthManager fakeAuthManager();
  FakeFooService fakeFooService();
}

Now the main method for your test binary calls DaggerTestComponent.builder() instead of DaggerProductionComponent.builder(). Note that the test component interface can add provision handles to the fake instances (fakeAuthManager() and fakeFooService()) so that the test can access them to control the harness if necessary.

Do not override bindings by subclassing modules

You might think a simple way to replace bindings in a testing component is to override modules’ @Provides methods in a subclass. Then, when you create an instance of your Dagger component, you can pass in instances of the modules it uses. (You do not have to pass instances for modules with no-arg constructors or those without instance methods, but you can.) For example:

@Component(modules = {AuthModule.class, /* … */})
interface MyApplicationComponent { /* … */ }

@Module
class AuthModule {
  @Provides AuthManager authManager(AuthManagerImpl impl) {
    return impl;
  }
}

class FakeAuthModule extends AuthModule {
  @Override
  AuthManager authManager(AuthManagerImpl impl) {
    return new FakeAuthManager();
  }
}

MyApplicationComponent testingComponent = DaggerMyApplicationComponent.builder()
    .authModule(new FakeAuthModule())
    .build();

But there are problems with this approach:

  • Using a module subclass cannot change the static shape of the binding graph: it cannot add or remove bindings, or change bindings’ dependencies. Specifically:

    • Overriding a @Provides method can’t change its parameter types, and narrowing the return type has no effect on the binding graph as Dagger understands it. In the example above, testingComponent still requires a binding for AuthManagerImpl and all its dependencies, even though they are not used.

    • Similarly, the overriding module cannot add bindings to the graph, including new multibinding contributions (although you can still override a SET_VALUES method to return a different set). Any new @Provides methods in the subclass are silently ignored by Dagger. Practically, this means that your fakes cannot take advantage of dependency injection.

  • @Provides methods that are overridable in this way cannot be static, so their module instances cannot be elided. This will affect runtime performance of your production code as well.

  • This method is brittle and usually makes code changes difficult. Most users won’t expect the @Provides methods to be overridden by a test, and so adding new dependencies will break tests even when it would be a functional no-op in Dagger.

Organize modules for testability

Module classes are a kind of utility class: a collection of independent @Provides methods, each of which may be used by the injector to provide some type used by the application.

(Although several @Provides methods may be related in that one depends on a type provided by another, they typically do not explicitly call each other or rely on the same mutable state. Some @Provides methods do refer to the same instance field, in which case they are not in fact independent. The advice given here treats @Provides methods as utility methods anyway because it leads to modules that can be readily substituted for testing.)

So how do you decide which @Provides methods should go together into one module class?

One way to think about it is to classify bindings into published bindings and internal bindings, and then to further decide which of the published bindings has reasonable alternatives.

Published bindings are those that provide functionality that is used by other parts of the application. Types like AuthManager or User or DocDatabase are published: they are bound in a module so that the rest of the application can use them.

Internal bindings are the rest: bindings that are used in the implementation of some published type and that are not meant to be used except as part of it. For example, the bindings for the configuration for the OAuth client ID or the OAuthKeyStore are intended to be used only by the OAuth implementation of AuthManager, and not by the rest of the application. These bindings are usually for package-private types or are qualified with package- private qualifiers.

Some published bindings will have reasonable alternatives, especially for testing, and others will not. For example, there are likely to be alternative bindings for a type like AuthManager: one for testing, others for different authentication/authorization protocols.

But on the other hand, if the AuthManager interface has a method that returns the currently logged-in user, you might want to publish a binding that provides User by simply calling getCurrentUser() on the AuthManager. That published binding is unlikely to ever need an alternative.

Once you’ve classified your bindings into published bindings with reasonable alternatives, published bindings without reasonable alternatives, and internal bindings, consider arranging them into modules like this:

  • One module for each published binding with a reasonable alternative. (If you are also writing the alternatives, each one gets its own module.) That module contains exactly one published binding, as well as all of the internal bindings that that published binding requires.

  • All published bindings with no reasonable alternatives go into modules organized along functional lines.

  • The published-binding modules should each include the no-reasonable-alternative modules that require the public bindings each provides.

It’s a good idea to document each module by describing the published bindings it provides.

Here’s an example using the auth domain. If there is an AuthManager interface, it might have an OAuth implementation and a fake implementation for testing. As above, there might be an obvious binding for the current user that you wouldn’t expect to change among configurations.

/**
 * Provides auth bindings that will not change in different auth configurations,
 * such as the current user.
 */
@Module
class AuthModule {
  @Provides static User currentUser(AuthManager authManager) {
    return authManager.currentUser();
  }
  // Other bindings that don’t differ among AuthManager implementations.
}

/** Provides a {@link AuthManager} that uses OAuth. */
@Module(includes = AuthModule.class) // Include no-alternative bindings.
class OAuthModule {
  @Provides static AuthManager authManager(OAuthManager authManager) {
    return authManager;
  }
  // Other bindings used only by OAuthManager.
}

/** Provides a fake {@link AuthManager} for testing. */
@Module(includes = AuthModule.class) // Include no-alternative bindings.
class FakeAuthModule {
  @Provides static AuthManager authManager(FakeAuthManager authManager) {
    return authManager;
  }
  // Other bindings used only by FakeAuthManager.
}

Then your production configuration will use the real modules, and the testing configuration the fake modules, as described above.

Unit tests and manual instantiation unit tests

For smaller unit tests, it might seem like a good idea to just avoid using Dagger entirely and just instantiate objects by calling the @Inject constructor. However, there are downsides to this approach. See this testing philosophy discussion for those downsides.

It is generally recommended to create a Dagger component in your tests to instantiate objects, whether that is a larger test component for many tests or a small focused test component for the individual test.