How do you override a module/dependency in a unit

2019-01-08 06:06发布

I have a simple Android activity with a single dependency. I inject the dependency into the activity's onCreate like this:

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

In my ActivityUnitTestCase I want to override the dependency with a Mockito mock. I assume I need to use a test-specific module which provides the mock, but I can't figure out how to add this module to the object graph.

In Dagger 1.x this is apparently done with something like this:

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

What's the Dagger 2.0 equivalent of the above?

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

8条回答
SAY GOODBYE
2楼-- · 2019-01-08 06:41

It seems I've found yet another way and it's working so far.

First, a component interface that is not a component itself:

MyComponent.java

interface MyComponent {
    Foo provideFoo();
}

Then we have two different modules: actual one and testing one.

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 to use 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 application we do this:

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

While in 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 methods of MyModule into TestModule, but it can be done by having MyModule inside TestModule and use MyModule's methods unless they are directly set from 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;
    }
}
查看更多
可以哭但决不认输i
3楼-- · 2019-01-08 06:43

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:

@Module
public 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).

@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'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:

@Before
public 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 :)

查看更多
登录 后发表回答