How to mock dependencies in Unit, Integration and Functional tests; Dagger, Robolectric and Instrumentation

How to mock dependencies in Unit, Integration and Functional tests; Dagger, Robolectric and Instrumentation
Unknown Ancient Woman Sculpture.

Initially, this was a comment to Robolectric and Dagger 2 question on Reddit but it became so huge that I decided to turn it into a blog post, have a nice read!


DI frameworks & Unit tests

In Unit test you usually test one class/method in isolation. You mock its dependencies if they're classes with behavior eg RestApi, DataManager, etc and use real classes (or mocks too) if they're just kind of "value classes" eg User, Message, etc.

That means, that usually, you don't even need to use DI frameworks in your Unit tests because you're testing not the integration of several classes but one target class/method. Your target class should accept dependencies in some way:

  • Via constructor (preferable way)
  • Via methods/fields

No DI framework should be required for Unit tests in 99% of cases, usually, only things like Activity, Fragment, View or Service which actually require graph of dependencies after creation can interact with DI framework. And even then, you can write Unit tests for them without DI framework, though I'd suggest MVP/etc to move away any logic from Android Framework classes and simply don't cover them with Unit tests, but cover with Functional (UI) tests.

DI frameworks & Integration tests

Usually, you don't need DI frameworks for Integration tests too because you simply combine real implementations of some classes and test their integration, if your code is DI-friendly, you should be able to pass required dependencies without DI framework.

But, if you really need to provide mocked dependencies via DI framework and at the same time you use Robolectric, see info below.

DI frameworks & Functional (UI) tests

Finally, this type of tests can really require mock dependencies provided via DI framework since basically, you test whole app, not small set of classes.

If you need to provide mocked dependencies via DI framework in instrumentation tests (Espresso, Robotium, just some instrumentation test, etc) see info below.

How to mock and inject dependencies in tests with Dagger 2 and Robolectric?

(Usually applicable for Integration tests)

Main idea: you can have custom Application class for tests under Robolectric and mock dependencies there.

In application class you can have a method that return Builder of DaggerAppComponent and then have overridden application classes for Integration tests!

Main application class

public class MyApp extends Application {

  @NonNull // Initialized in onCreate.
  AppCompontent appComponent;

  @Override
  public void onCreate() {
    appComponent = prepareAppComponent().build();
  }

  // Here is the trick, we allow extend application class and modify AppComponent.
  @NonNull
  protected DaggerAppComponent.Builder prepareAppComponent() {
    return new DaggerAppComponent.Builder();
  }
}

Application class for Integration tests

public class MyIntegrationTestApp extends MyApp {

  @Override
  @NonNull
  protected DaggerAppComponent.Builder prepareAppComponent() {
    return super.prepareAppComponent()
      .someModule(new SomeModule() {
        @Override
        public SomeDependency provideSomeDependency(@NonNull SomeArgs someArgs) {
          return mock(SomeDependency.class); // You can provide any kind of mock you need.
        }
      })
  }
}

Then you can provide this application class via custom RobolectricGradleTestRunner

Custom Robolectric test runner with custom application class

public class IntegrationRobolectricTestRunner extends RobolectricGradleTestRunner {

    // This value should be changed as soon as Robolectric will support newer api.
    private static final int SDK_EMULATE_LEVEL = 21;

    public IntegrationRobolectricTestRunner(@NonNull Class<?> clazz) throws Exception {
        super(clazz);
    }

    @Override
    public Config getConfig(@NonNull Method method) {
        final Config defaultConfig = super.getConfig(method);
        return new Config.Implementation(
                new int[]{SDK_EMULATE_LEVEL},
                defaultConfig.manifest(),
                defaultConfig.qualifiers(),
                defaultConfig.packageName(),
                defaultConfig.resourceDir(),
                defaultConfig.assetDir(),
                defaultConfig.shadows(),
                MyIntegrationTestApp.class, // Here is the trick, we change application class to one with mocks.
                defaultConfig.libraries(),
                defaultConfig.constants() == Void.class ? BuildConfig.class : defaultConfig.constants()
        );
    }
}

How to mock and inject dependencies in tests with Dagger 2 in Instrumentation tests?

While guys from Google suggest us use flavors I don't suggest you use them. Because the more flavors you have — the longer builds you will have, the more you'll hate Gradle and all that complicated build process. If you can avoid flavors — avoid them.

Main idea: same as for tests under Robolectric — change Application class, but for Instrumentation tests.

To do so you need to create custom Instrumentation test runner and then apply in the build.gradle

public class CustomInstrumentationTestRunner extends AndroidJUnitRunner {

  @Override
  @NonNull
  public Application newApplication(@NonNull ClassLoader cl, 
                                    @NonNull String className, 
                                    @NonNull Context context)
                                    throws InstantiationException, 
                                    IllegalAccessException, 
                                    ClassNotFoundException {
    return Instrumentation.newApplication(CustomApp.class, context);
  }
}

And then apply runner in build.gradle

android {
  defaultConfig {
    testInstrumentationRunner 'a.b.c.CustomInstrumentationTestRunner'
  }
}

Code example: I've updated #qualitymatters app and put an Analytics there and showed a way to mock in in Unit, Integration and Functional tests, because we all need Analytics in the real app, but we don't want it to work in tests! And, as you might notice, no flavors were added.

You can take a look at the PR with changes.


// Phew… hope you haven't lost in my thoughts and it was helpful to you!