【问题标题】:How can I replace Activity scoped dependencies with mocks using Dagger2如何使用 Dagger2 用模拟替换 Activity 范围的依赖项
【发布时间】:2015-11-23 17:34:33
【问题描述】:

我的 Activity 中有一个作用域依赖项,我想用一些模拟来测试该 Activity。我已经阅读了不同的方法,建议在测试期间用测试组件替换 Application 组件,但我想要的是替换 Activity 组件。

例如,我想在我的 MVP 设置中针对模拟演示者测试 Activity。

我认为在Activity上调用setComponent()替换组件是行不通的,因为Activity依赖已经通过字段注入注入了,所以在测试的时候会使用真实的对象。

我该如何解决这个问题?匕首1呢?有同样的问题吗?

【问题讨论】:

  • 我已经这样做了一次stackoverflow.com/a/30736436/2413303 但是有了足够多的模块,这将变得难以维护(因为每个模块都变成了参数化,您需要使用正确的提供者明确地创建所有模块)...我正在尝试找出更好的解决方案。问题是您无法在 Dagger2 中扩展模块,即使您确实可以选择使用 overrides=true 作为 Dagger1 中的模块。

标签: android unit-testing dependency-injection dagger dagger-2


【解决方案1】:

我找到了解决问题的以下帖子: http://blog.sqisland.com/2015/04/dagger-2-espresso-2-mockito.html

您需要先允许修改活动的组件:

@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;
}

并在测试用例中修改

@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);
  }

【讨论】:

    【解决方案2】:

    你不能覆盖 Dagger2 中的模块 [编辑:你可以,只是不要在模拟上指定 @Provides 注释),这显然是正确的解决方案:只需使用 @987654323 @并完成它!

    如果您认为模拟是不可能的,那么我会看到这个问题的两种可能的解决方案。您可以使用这些模块来包含一个可插入的“提供程序”,该“提供程序”可以更改其实现(我不赞成这样做,因为它太冗长了!)

    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();
    

    或者您可以将提供的类和注入目标引入它们自己的“元组件”接口,您的ApplicationComponent 和您的TestApplicationComponent 扩展自该接口。

    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 {
    }
    

    第三种解决方案是像@vaughandroid 的回答那样扩展模块。参考那个,这才是正确的做法。

    至于活动范围的组件...和我在这里提到的一样,它只是一个不同的范围,真的。

    【讨论】:

    • 官方不支持,但是你可以扩展模块。只需扩展类并覆盖提供者方法!这绝对不应该在生产代码中完成,但我发现它可以很好地插入用于测试的模拟。
    • @vaughandroid 我的目标是通过以某种方式换出模块,用void inject(Something something); 调用替换字段注入提供的依赖项,这允许吗?我在这里提到的两个可以,我认为第二个是更清洁的解决方案。
    • 无论你使用构造函数、字段还是方法注入都没有区别。我还没有遇到任何陷阱,尽管我确信有一些。
    • 我已经在这里stackoverflow.com/a/32330072/213727写了我自己的答案,其中包括一个例子和一个简短的解释。
    【解决方案3】:

    注入组件

    首先,您创建一个静态类来充当 Activity 的工厂。我的看起来有点像这样:

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

    然后在您的活动中执行ActivityComponentFactory.getInstance().createActivityComponent().inject(this);

    对于测试,您可以在创建 Activity 之前替换方法中的工厂。

    提供模拟

    正如@EpicPandaForce 的回答所表明的那样,官方支持的方式目前涉及大量样板代码和复制/粘贴代码。 Dagger 2 团队需要提供一种更简单的方法来部分覆盖模块。

    在他们这样做之前,这是我的非正式方式:只需扩展模块

    假设您想用模拟替换您的 ListViewPresenter。假设您有一个如下所示的 PresenterModule:

    @Module @ActivityScope
    public class PresenterModule {
    
        @ActivityScope
        public ListViewPresenter provideListViewPresenter() {
            return new ListViewPresenter();
        }
    
        @ActivityScope
        public SomeOtherPresenter provideSomeOtherPresenter() {
            return new SomeOtherPresenter();
        }
    }
    

    您可以在测试设置中执行此操作:

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

    ...而且它可以正常工作

    请注意,您不必在 @Override 方法中包含 @Provides 注释。事实上,如果你这样做了,那么 Dagger 2 代码生成将会失败。

    这是因为模块只是简单的工厂——生成的组件类负责缓存作用域实例的实例。 @Scope 注解由代码生成器使用,但在运行时无关紧要。

    【讨论】:

    • 我完全没有想到这一点。我猜匿名子类没有被检查覆盖,而真正的子类是?这很有趣,因为这意味着这是一个错误,但实际上它从一开始就应该是一个功能。
    • 类是否命名无关紧要 - 代码生成器会查找方法上是否存在 @Provides 注释。如果那不存在,它会忽略它们。在发布一些官方指南/额外功能之前,我肯定会认为这种方法是一种临时破解。一些复杂的场景可能会遇到问题(但我还没有遇到过)。
    • 等等,只要不指定第二个@Provides注解就可以覆盖模块中的方法?
    • 是的,但您不能更改 @Provides 合同 - 例如如果原始方法被标记为@ApplicationScope @Named("foo"),那么任何覆盖也将采用相同的合同。
    • 如果它能让你感觉好些,我也很想念它。我的第一次尝试涉及创建子模块的部分模拟! :)
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-03-19
    • 2022-12-10
    • 1970-01-01
    • 1970-01-01
    • 2014-09-23
    • 1970-01-01
    相关资源
    最近更新 更多