How to 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?

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.
*/
// @VisibleForTesting
public 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)
@LargeTest
public 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(...)
}
}

Override dependencies in Dagger Module

You're binding each as Navigator. I believe you need to use a different return type on your shadowed binding.

Alternatively, you could try something with Qualifiers. Defining a custom qualifier is easy; you should be able to find examples online. I'd share one, but I'm on my phone right now.

This answer has been accepted, so I'd like to add some code to make it more "complete". Here's an example of a custom "Qualifier" (Kotlin)

import javax.inject.Qualifier

@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class DelayQualifier

Usage:

@Module object {
@Provides @DelayQualifier @JvmStatic
fun provideDelay(): Long = if (BuildConfig.DEBUG) 1L else 3L
}

@ActivityScoped
class SignupViewModelFactory @Inject constructor(
@param:DelayQualifier private val delay: Long
) : ViewModelProvider.Factory { ... }

This is the only Long I'm currently injecting in my project, so I don't need the qualifier. But if I decide I want more Longs, I'll regret not qualifying this one.

Migrating to Dagger 2.12 and test module overrides

I ended up solving it by simply not annotating the TestAppModule with @Module. This achieves partial mocking.

public class TestAppModule extends AppModule {

@Override
public UserDao provideUserDao() {
return mock(UserDao.class);
}
}

Android Unit Tests with Dagger 2

This is currently impossible with Dagger 2 (as of v2.0.0) without some workarounds. You can read about it here.

More about possible workarounds:

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

  • Creating test dependencies when using Dagger2



Related Topics



Leave a reply



Submit