How do you override a module/dependency in a unit test with Dagger 2.0? How do you override a module/dependency in a unit test with Dagger 2.0? android android

How do you override a module/dependency in a unit test with Dagger 2.0?


Probably this is more a workaround that proper support for test module overriding, but it allows to override production modules with test one. The code snippets below shows simple case when you have just one component and one module, but this should work for any scenario. It requires a lot of boilerplate and code repetition so be aware of this. I'm sure there'll be a better way to achieve this in the future.

I've also created a project with examples for Espresso and Robolectric. This answer is based on code contained in the project.

The solution requires two things:

  • provide additional setter for @Component
  • test component must extend the production component

Assume we've simple Application like below:

public class App extends Application {    private AppComponent mAppComponent;    @Override    public void onCreate() {        super.onCreate();        mAppComponent = DaggerApp_AppComponent.create();    }    public AppComponent component() {        return mAppComponent;    }    @Singleton    @Component(modules = StringHolderModule.class)    public interface AppComponent {        void inject(MainActivity activity);    }    @Module    public static class StringHolderModule {        @Provides        StringHolder provideString() {            return new StringHolder("Release string");        }    }}

We've to add additional method to App class. This allows us to replace the production component.

/** * Visible only for testing purposes. */// @VisibleForTestingpublic void setTestComponent(AppComponent appComponent) {    mAppComponent = appComponent;}

As you can see the StringHolder object contains "Release string" value. This object is injected to the MainActivity.

public class MainActivity extends ActionBarActivity {    @Inject    StringHolder mStringHolder;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        ((App) getApplication()).component().inject(this);    }}

In our tests we want to provide StringHolder with "Test string". We've to set the test component in App class before the MainActivity is created - because StringHolder is injected in the onCreate callback.

In Dagger v2.0.0 components can extend other interfaces. We can leverage this to create our TestAppComponent which extends AppComponent.

@Component(modules = TestStringHolderModule.class)interface TestAppComponent extends AppComponent {}

Now we're able to define our test modules e.g. TestStringHolderModule. The last step is to set the test component using previously added setter method in App class. It's important to do this before the activity is created.

((App) application).setTestComponent(mTestAppComponent);

Espresso

For Espresso I've created custom ActivityTestRule which allows to swap the component before the activity is created. You can find code for DaggerActivityTestRule here.

Sample test with Espresso:

@RunWith(AndroidJUnit4.class)@LargeTestpublic class MainActivityEspressoTest {    public static final String TEST_STRING = "Test string";    private TestAppComponent mTestAppComponent;    @Rule    public ActivityTestRule<MainActivity> mActivityRule =            new DaggerActivityTestRule<>(MainActivity.class, new OnBeforeActivityLaunchedListener<MainActivity>() {                @Override                public void beforeActivityLaunched(@NonNull Application application, @NonNull MainActivity activity) {                    mTestAppComponent = DaggerMainActivityEspressoTest_TestAppComponent.create();                    ((App) application).setTestComponent(mTestAppComponent);                }            });    @Component(modules = TestStringHolderModule.class)    interface TestAppComponent extends AppComponent {    }    @Module    static class TestStringHolderModule {        @Provides        StringHolder provideString() {            return new StringHolder(TEST_STRING);        }    }    @Test    public void checkSomething() {        // given        ...        // when        onView(...)        // then        onView(...)                .check(...);    }}

Robolectric

It's much easier with Robolectric thanks to the RuntimeEnvironment.application.

Sample test with Robolectric:

@RunWith(RobolectricGradleTestRunner.class)@Config(emulateSdk = 21, reportSdk = 21, constants = BuildConfig.class)public class MainActivityRobolectricTest {    public static final String TEST_STRING = "Test string";    @Before    public void setTestComponent() {        AppComponent appComponent = DaggerMainActivityRobolectricTest_TestAppComponent.create();        ((App) RuntimeEnvironment.application).setTestComponent(appComponent);    }    @Component(modules = TestStringHolderModule.class)    interface TestAppComponent extends AppComponent {    }    @Module    static class TestStringHolderModule {        @Provides        StringHolder provideString() {            return new StringHolder(TEST_STRING);        }    }    @Test    public void checkSomething() {        // given        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);        // when        ...        // then        assertThat(...)    }}


As @EpicPandaForce rightly says, you can't extend Modules. However, I came up with a sneaky workaround for this which I think avoids a lot of the boilerplate which the other examples suffer from.

The trick to 'extending' a Module is to create a partial mock, and mock out the provider methods which you want to override.

Using Mockito:

MyModule module = Mockito.spy(new MyModule());Mockito.doReturn("mocked string").when(module).provideString();MyComponent component = DaggerMyComponent.builder()        .myModule(module)        .build();app.setComponent(component);

I created this gist here to show a full example.

EDIT

It turns out you can do this even without a partial mock, like so:

MyComponent component = DaggerMyComponent.builder()        .myModule(new MyModule() {            @Override public String provideString() {                return "mocked string";            }        })        .build();app.setComponent(component);


The workaround proposed by @tomrozb is very good and put me on the right track, but my problem with it was that it exposed a setTestComponent() method in the PRODUCTION Application class. I was able to get this working slightly differently, such that my production application doesn't have to know anything at all about my testing environment.

TL;DR - Extend your Application class with a test application that uses your test component and module. Then create a custom test runner that runs on the test application instead of your production application.


EDIT: This method only works for global dependencies (typically marked with @Singleton). If your app has components with different scope (e.g. per activity) then you'll either need to create subclasses for each scope, or use @tomrozb's original answer. Thanks to @tomrozb for pointing this out!


This example uses the AndroidJUnitRunner test runner but this could probably be adapted to Robolectric and others.

First, my production application. It looks something like this:

public class MyApp extends Application {    protected MyComponent component;    public void setComponent() {        component = DaggerMyComponent.builder()                .myModule(new MyModule())                .build();        component.inject(this);    }    public MyComponent getComponent() {        return component;    }    @Override    public void onCreate() {        super.onCreate();        setComponent();    }}

This way, my activities and other class that use @Inject simply have to call something like getApp().getComponent().inject(this); to inject themselves into the dependency graph.

For completeness, here is my component:

@Singleton@Component(modules = {MyModule.class})public interface MyComponent {    void inject(MyApp app);    // other injects and getters}

And my module:

@Modulepublic class MyModule {    // EDIT: This solution only works for global dependencies    @Provides @Singleton    public MyClass provideMyClass() { ... }    // ... other providers}

For the testing environment, extend your test component from your production component. This is the same as in @tomrozb's answer.

@Singleton@Component(modules = {MyTestModule.class})public interface MyTestComponent extends MyComponent {    // more component methods if necessary}

And the test module can be whatever you want. Presumably you'll handle your mocking and stuff in here (I use Mockito).

@Modulepublic class MyTestModule {    // EDIT: This solution only works for global dependencies    @Provides @Singleton    public MyClass provideMyClass() { ... }    // Make sure to implement all the same methods here that are in MyModule,     // even though it's not an override.}

So now, the tricky part. Create a test application class that extends from your production application class, and override the setComponent() method to set the test component with the test module. Note that this can only work if MyTestComponent is a descendant of MyComponent.

public class MyTestApp extends MyApp {    // Make sure to call this method during setup of your tests!    @Override    public void setComponent() {        component = DaggerMyTestComponent.builder()                .myTestModule(new MyTestModule())                .build();        component.inject(this)    }}

Make sure you call setComponent() on the app before you begin your tests to make sure the graph is set up correctly. Something like this:

@Beforepublic void setUp() {    MyTestApp app = (MyTestApp) getInstrumentation().getTargetContext().getApplicationContext();    app.setComponent()    ((MyTestComponent) app.getComponent()).inject(this)}

Finally, the last missing piece is to override your TestRunner with a custom test runner. In my project I was using the AndroidJUnitRunner but it looks like you can do the same with Robolectric.

public class TestRunner extends AndroidJUnitRunner {    @Override    public Application newApplication(@NonNull ClassLoader cl, String className, Context context)            throws InstantiationException, IllegalAccessException, ClassNotFoundException {        return super.newApplication(cl, MyTestApp.class.getName(), context);    }}

You'll also have to update your testInstrumentationRunner gradle, like so:

testInstrumentationRunner "com.mypackage.TestRunner"

And if you're using Android Studio, you'll also have to click Edit Configuration from the run menu and enter the name of your test runner under "Specific instrumentation runner".

And that's it! Hopefully this information helps somebody :)