How can I replace Activity scoped dependencies wit

2019-04-09 14:01发布

问题:

I have a scoped dependency in my Activity and I want to test that activity with some mocks. I have read about different approach that suggest to replace Application component with a test component during the test, but what I want is to replace the Activity component.

For example, I want to test the Activity against mock presenter in my MVP setup.

I believe that replacing component by calling setComponent() on Activity will not work, because Activity dependencies already injected via field injection, so during the test, real object will be used.

How can I resolve this issue? What about Dagger1? Is it has the same issue?

回答1:

Injecting the Component

First, you create a static class to act as a factory for your Activity. Mine looks a little like this:

public class ActivityComponentFactory {

    private static ActivityComponentFactory sInstance;

    public static ActivityComponentFactory getInstance() {
        if(sInstance == null) sInstance = new ActivityComponentFactory();
        return sInstance;
    }

    @VisibleForTesting
    public static void setInstance(ActivityComponentFactory instance) {
        sInstance = instance;
    }

    private ActivityComponentFactory() {
        // Singleton
    }

    public ActivityComponent createActivityComponent() {
        return DaggerActivityComponent.create();
    }
}

Then just do ActivityComponentFactory.getInstance().createActivityComponent().inject(this); inside your Activities.

For testing, you can replace the factory in your method, before the Activity is created.

Providing mocks

As @EpicPandaForce's answer makes clear, doing this the officially-supported way currently involves a lot of boilerplate and copy/pasted code. The Dagger 2 team need to provide a simpler way of partially overriding Modules.

Until they do though, here's my unnoficial way: Just extend the module.

Let's say you want to replace your ListViewPresenter with a mock. Say you have a PresenterModule which looks like this:

@Module @ActivityScope
public class PresenterModule {

    @ActivityScope
    public ListViewPresenter provideListViewPresenter() {
        return new ListViewPresenter();
    }

    @ActivityScope
    public SomeOtherPresenter provideSomeOtherPresenter() {
        return new SomeOtherPresenter();
    }
}

You can just do this in your test setup:

ActivityComponentFactory.setInstance(new ActivityComponentFactory() {
    @Override
    public ActivityComponent createActivityComponent() {
        return DaggerActivityComponent.builder()
                .presenterModule(new PresenterModule() {
                    @Override
                    public ListViewPresenter provideListViewPresenter() {
                        // Note you don't have to use Mockito, it's just what I use
                        return Mockito.mock(ListViewPresenter.class);
                    }
                })
                .build();
    }
});

...and it just works!

Note that you don't have to include the @Provides annotation on the @Override method. In fact, if you do then the Dagger 2 code generation will fail.

This works because the Modules are just simple factories - the generated Component classes take care of caching instances of scoped instances. The @Scope annotations are used by the code generator, but are irrelevant at runtime.



回答2:

You cannot override modules in Dagger2 [EDIT: you can, just don't specify the @Provides annotation on the mock), which would obviously be the proper solution: just use the builder().somethingModule(new MockSomethingModule()).build() and be done with it!

If you thought mocking is not possible, then I would have seen two possible solutions to this problem. You can either use the modules to contain a pluggable "provider" that can have its implementation changed (I don't favor this because it's just too verbose!)

public interface SomethingProvider {
    Something something(Context context);
}

@Module
public class SomethingModule {
    private SomethingProvider somethingProvider;

    public SomethingModule(SomethingProvider somethingProvider) {
        this.somethingProvider = somethingProvider;
    }

    @Provides
    @Singleton
    public Something something(Context context) {
        return somethingProvider.something(context);
    }
}

public class ProdSomethingProvider implements SomethingProvider {
    public Something something(Context context) {
        return new SomethingImpl(context);
    }
}

public class TestSomethingProvider implements SomethingProvider {
    public Something something(Context context) {
        return new MockSomethingImpl(context);
    }
}

SomethingComponent somethingComponent = DaggerSomethingComponent.builder()
    .somethingModule(new SomethingModule(new ProdSomethingProvider()))
    .build();

Or you can bring the provided classes and injection targets out into their own "metacomponent" interface, which your ApplicationComponent and your TestApplicationComponent extend from.

public interface MetaApplicationComponent {
    Something something();

    void inject(MainActivity mainActivity);
}

@Component(modules={SomethingModule.class})
@Singleton
public interface ApplicationComponent extends MetaApplicationComponent {
}

@Component(modules={MockSomethingModule.class})
@Singleton
public interface MockApplicationComponent extends MetaApplicationComponent {
}

The third solution is to just extend the modules like in @vaughandroid 's answer. Refer to that, that is the proper way of doing it.

As for activity scoped components... same thing as I mentioned here, it's just a different scope, really.



回答3:

I've found the following post that solves the problem: http://blog.sqisland.com/2015/04/dagger-2-espresso-2-mockito.html

You need first to allow to modify the component of the activity:

@Override public void onCreate() {
  super.onCreate();
  if (component == null) {
    component = DaggerDemoApplication_ApplicationComponent
        .builder()
        .clockModule(new ClockModule())
        .build();
  }
}

public void setComponent(DemoComponent component) {
  this.component = component;
}

public DemoComponent component() {
  return component;
}

And modify it in the test case

@Before
  public void setUp() {
    Instrumentation instrumentation = InstrumentationRegistry.getInstrumentation();
    DemoApplication app
        = (DemoApplication) instrumentation.getTargetContext().getApplicationContext();
    TestComponent component = DaggerMainActivityTest_TestComponent.builder()
        .mockClockModule(new MockClockModule())
        .build();
    app.setComponent(component);
    component.inject(this);
  }