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

I have a simple Android activity with one dependency. I inject the dependency into onCreate activity as follows:

 Dagger_HelloComponent.builder() .helloModule(new HelloModule(this)) .build() .initialize(this); 

In my ActivityUnitTestCase I want to override the dependency using mockito mock. I assume that I need to use a test module that provides a layout, but I cannot figure out how to add this module to the object graph.

In dagger 1.x, this seems to be done with something like this :

 @Before public void setUp() { ObjectGraph.create(new TestModule()).inject(this); } 

What is the Dagger 2.0 equivalent above?

You can see my project and unit test here on GitHub .

+48
android dagger dagger-2
Nov 14 '14 at 21:44
source share
7 answers

This is probably more of a workaround that provides proper support for module testing, but it allows you to override production modules with test modules. The code snippets below show a simple case where you only have one component and one module, but this should work for any scenario. This requires a lot of repetition of patterns and codes, so keep that in mind. I am sure that in the future there will be a better way to achieve this.

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

The solution requires two things:

  • provide an extra kit for @Component
  • test component should expand production component

Suppose we have a simple Application , as shown 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 need to add an additional method to the App class. This allows us to replace the manufacturing component.

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

As you can see, the StringHolder object contains the value "Release string". This object is entered in 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 the StringHolder "Test string". We need to install the test component in the App class before creating the MainActivity , because StringHolder is introduced in the onCreate .

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

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

Now we can define our test modules, for example. TestStringHolderModule . The final step is to install the test component using the previously added setter method in the App class. It is important to do this before activity is created.

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

Espresso

For Espresso, I created a custom ActivityTestRule that allows you to exchange a component before creating an activity. You can find the code for the DaggerActivityTestRule here .

Espresso test example:

 @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

This is much easier with Robolectric thanks to RuntimeEnvironment.application .

Test example 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(...) } } 
+38
May 01 '15 at
source share

As @EpicPandaForce rightly says, you cannot extend modules. However, I came up with a nimble workaround for this, which I think avoids the large number of patterns that other examples suffer from.

The trick to "expanding" the module is to create a partial layout and mock up the provider methods that 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 meaning here to show a complete example.

EDIT

It turns out you can do this even without a partial layout, for example:

 MyComponent component = DaggerMyComponent.builder() .myModule(new MyModule() { @Override public String provideString() { return "mocked string"; } }) .build(); app.setComponent(component); 
+20
Aug 27 '15 at 15:12
source share

The workaround suggested by @tomrozb is very good and set me on the right path, but my problem was that it setTestComponent() method in the PRODUCTION Application class. I managed to get this to work a little differently, so my production application didn't need to know anything about my test environment.

TL; DR - Extend your application class with a test application that uses your test component and module. Then create your own test runner that runs in the test application, and not in the production application.




EDIT: This method only works for global dependencies (usually with @Singleton ). If your application has components with different scope (for example, for each action), you will need to create subclasses for each scope or use the original @tomrozb answer. Thanks to @tomrozb for pointing this out!




This example uses the AndroidJUnitRunner tester, but it could probably be adapted to Robolectric and others.

Firstly, 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(); } } 

So my actions and other classes that use @Inject just have to call something like getApp().getComponent().inject(this); to enter yourself in 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:

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

In a test environment, extend your test component from your production component. This is the same as @ tomrozb answer.

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

And the test module can be anything you want. Presumably, you will handle your bullying and things here (I use Mockito).

 @Module public 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 not an override. } 

So, the hard part. Create a test application class that extends from your production application class and override the setComponent() method to install the test component using 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) } } 

Before starting the tests, make sure you call setComponent() to make sure the graph is set up correctly. Something like that:

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

Finally, the last missing element is to override your TestRunner with a special test runner. In my project, I used 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 will also need to update testInstrumentationRunner gradle, for example:

 testInstrumentationRunner "com.mypackage.TestRunner" 

And if you use Android Studio, you also need to click "Edit Configuration" in the "Start" menu and enter the name of your test runner in the "Specific Controller" section.

And this! Hope this information helps someone :)

+8
Aug 14 '15 at 23:34
source share

I seem to have found another way, and it still works.

First, the component interface, which is not the component itself:

MyComponent.java

 interface MyComponent { Foo provideFoo(); } 

Then we have two different modules: actual and test.

Mymodule.java

 @Module class MyModule { @Provides public Foo getFoo() { return new Foo(); } } 

TestModule.java

 @Module class TestModule { private Foo foo; public void setFoo(Foo foo) { this.foo = foo; } @Provides public Foo getFoo() { return foo; } } 

And we have two components for using these two modules:

MyRealComponent.java

 @Component(modules=MyModule.class) interface MyRealComponent extends MyComponent { Foo provideFoo(); // without this dagger will not do its magic } 

MyTestComponent.java

 @Component(modules=TestModule.class) interface MyTestComponent extends MyComponent { Foo provideFoo(); } 

In the application, we do the following:

 MyComponent component = DaggerMyRealComponent.create(); <...> Foo foo = component.getFoo(); 

In the test code we use:

 TestModule testModule = new TestModule(); testModule.setFoo(someMockFoo); MyComponent component = DaggerMyTestComponent.builder() .testModule(testModule).build(); <...> Foo foo = component.getFoo(); // will return someMockFoo 

The problem is that we have to copy all the MyModule methods into the TestModule, but this can be done using the MyModule inside the TestModule and using the MyModule methods if they are not installed directly from the outside. Like this:

TestModule.java

 @Module class TestModule { MyModule myModule = new MyModule(); private Foo foo = myModule.getFoo(); public void setFoo(Foo foo) { this.foo = foo; } @Provides public Foo getFoo() { return foo; } } 
+2
Feb 02 '16 at 17:22
source share

THIS RESPONSE IS SHIPPED. READ BELOW CHANGES.

Uncomfortable enough, you cannot spread from the module, or you will get the following compilation error:

 Error:(24, 21) error: @Provides methods may not override another method. Overrides: Provides retrofit.Endpoint hu.mycompany.injection.modules.application.domain.networking.EndpointModule.mySe‌​rverEndpoint() 

So you can’t just expand the “layout” module and replace the original module. No, it's not that simple. And given that you design your components in such a way that they directly bind modules by class, you cannot just do a “TestComponent” because it means you have to rethink all the scratches and you have to compose a component for each variation! Clearly, this is not an option.

So, on a smaller scale, what I ended up with is making a “provider”, which I give to the module, which determines whether I choose the layout or type of production.

 public interface EndpointProvider { Endpoint serverEndpoint(); } public class ProdEndpointProvider implements EndpointProvider { @Override public Endpoint serverEndpoint() { return new ServerEndpoint(); } } public class TestEndpointProvider implements EndpointProvider { @Override public Endpoint serverEndpoint() { return new TestServerEndpoint(); } } @Module public class EndpointModule { private Endpoint serverEndpoint; private EndpointProvider endpointProvider; public EndpointModule(EndpointProvider endpointProvider) { this.endpointProvider = endpointProvider; } @Named("server") @Provides public Endpoint serverEndpoint() { return endpointProvider.serverEndpoint(); } } 

EDIT: As you can see from the error message, you CANNOT override another method using the annotated @Provides method, but this does not mean that you cannot override the annotated @Provides method: (

All this magic was in vain! You can simply extend the module without putting @Provides in the method and it works ... See @vaughandroid's answer.

+1
Jun 09 '15 at 15:00
source share

Can you guys check my solution, I included an example of a subcomponent: https://github.com/nongdenchet/android-mvvm-with-tests . Thanks @vaughandroid, I borrowed your basic methods. Here is the main point:

  • I am creating a class to create a subcomponent. My user application will also contain an instance of this class:

     // The builder class public class ComponentBuilder { private AppComponent appComponent; public ComponentBuilder(AppComponent appComponent) { this.appComponent = appComponent; } public PlacesComponent placesComponent() { return appComponent.plus(new PlacesModule()); } public PurchaseComponent purchaseComponent() { return appComponent.plus(new PurchaseModule()); } } // My custom application class public class MyApplication extends Application { protected AppComponent mAppComponent; protected ComponentBuilder mComponentBuilder; @Override public void onCreate() { super.onCreate(); // Create app component mAppComponent = DaggerAppComponent.builder() .appModule(new AppModule()) .build(); // Create component builder mComponentBuilder = new ComponentBuilder(mAppComponent); } public AppComponent component() { return mAppComponent; } public ComponentBuilder builder() { return mComponentBuilder; } } // Sample using builder class: public class PurchaseActivity extends BaseActivity { ... @Override protected void onCreate(Bundle savedInstanceState) { ... // Setup dependency ((MyApplication) getApplication()) .builder() .purchaseComponent() .inject(this); ... } } 
  • I have a custom TestApplication that extends the MyApplication class above. This class contains two methods for replacing the root component and the builder:

     public class TestApplication extends MyApplication { public void setComponent(AppComponent appComponent) { this.mAppComponent = appComponent; } public void setComponentBuilder(ComponentBuilder componentBuilder) { this.mComponentBuilder = componentBuilder; } } 
  • Finally, I will try to ridicule or drown out the dependency of the module and the builder in order to provide a fake activity dependency:

     @MediumTest @RunWith(AndroidJUnit4.class) public class PurchaseActivityTest { @Rule public ActivityTestRule<PurchaseActivity> activityTestRule = new ActivityTestRule<>(PurchaseActivity.class, true, false); @Before public void setUp() throws Exception { PurchaseModule stubModule = new PurchaseModule() { @Provides @ViewScope public IPurchaseViewModel providePurchaseViewModel(IPurchaseApi purchaseApi) { return new StubPurchaseViewModel(); } }; // Setup test component AppComponent component = ApplicationUtils.application().component(); ApplicationUtils.application().setComponentBuilder(new ComponentBuilder(component) { @Override public PurchaseComponent purchaseComponent() { return component.plus(stubModule); } }); // Run the activity activityTestRule.launchActivity(new Intent()); } 
0
Oct 25 '15 at 9:44
source share

With Dagger2, you can pass a specific module (TestModule) to a component using the generated api constructor.

 ApplicationComponent appComponent = Dagger_ApplicationComponent.builder() .helloModule(new TestModule()) .build(); 

Note that Dagger_ApplicationComponent is a generated class with the new @Component annotation.

-four
Nov 15 '14 at 12:04
source share



All Articles