首页
学习
活动
专区
工具
TVP
发布
社区首页 >问答首页 >这是在单元测试中使用Dagger 2 for Android app来覆盖mock/fakes依赖关系的正确方式吗?

这是在单元测试中使用Dagger 2 for Android app来覆盖mock/fakes依赖关系的正确方式吗?
EN

Stack Overflow用户
提问于 2016-03-03 19:47:17
回答 2查看 2.4K关注 0票数 19

对于“常规的”Java项目,用mock/fake覆盖单元测试中的依赖关系是很容易的。您必须简单地构建Dagger组件并将其提供给驱动您的应用程序的“main”类。

For Android things is 不是那么简单的,我已经寻找了很长一段时间来寻找合适的例子,但我无法找到,所以我不得不创建自己的实现,我将非常感谢反馈这是使用Dagger2的正确方式,还是有更简单/更优雅的方法来覆盖依赖项。

这里是解释(project source can be found on github):

假设我们有一个简单的应用程序,它使用带有单个模块的单个Dagger组件的Dagge2,我们想要创建使用JUnit4、MockitoEspresso的android单元测试

MyApp Application类中,组件/注入器的初始化方式如下:

代码语言:javascript
复制
public class MyApp extends Application {
    private MyDaggerComponent mInjector;

    public void onCreate() {
        super.onCreate();
        initInjector();
    }

    protected void initInjector() {
        mInjector = DaggerMyDaggerComponent.builder().httpModule(new HttpModule(new OkHttpClient())).build();

        onInjectorInitialized(mInjector);
    }

    private void onInjectorInitialized(MyDaggerComponent inj) {
        inj.inject(this);
    }

    public void externalInjectorInitialization(MyDaggerComponent injector) {
        mInjector = injector;

        onInjectorInitialized(injector);
    }

    ...

在上面的代码中:正常的应用程序启动通过调用创建注入器的initInjector()onCreate(),然后调用onInjectorInitialized()

为了从外部源(即单元测试) set注入器,单元测试需要调用externalInjectorInitialization()方法。

到现在为止还好。

让我们看看单元测试端的东西是什么样子的:

我们需要创建扩展MyApp类并用空方法覆盖initInjector的MyTestApp调用,以避免创建双注入器(因为我们将在单元测试中创建一个新的注入器):

代码语言:javascript
复制
public class MyTestApp extends MyApp {
    @Override
    protected void initInjector() {
        // empty
    }
}

然后,我们必须以某种方式用MyTestApp替换原始的MyApp。这是通过自定义测试运行器完成的:

代码语言:javascript
复制
public class MyTestRunner extends AndroidJUnitRunner {
    @Override
    public Application newApplication(ClassLoader cl,
                                      String className,
                                      Context context) throws InstantiationException,
            IllegalAccessException,
            ClassNotFoundException {


        return super.newApplication(cl, MyTestApp.class.getName(), context);
    }
}

..。在newApplication()中,我们有效地用测试类替换了原始的app类。

然后,我们必须告诉测试框架,我们已经拥有并希望使用我们的自定义测试运行器,因此在build.gradle中我们添加:

代码语言:javascript
复制
defaultConfig {
    ...
    testInstrumentationRunner 'com.bolyartech.d2overrides.utils.MyTestRunner'
    ...
}

当运行单元测试时,我们的原始MyApp将被替换为MyTestApp。现在,我们必须创建组件/注入器,并将其与模拟/假冒一起提供给externalInjectorInitialization()应用程序。为此,我们扩展了普通的ActivityTestRule:

代码语言:javascript
复制
@Rule
public ActivityTestRule<Act_Main> mActivityRule = new ActivityTestRule<Act_Main>(
        Act_Main.class) {


    @Override
    protected void beforeActivityLaunched() {
        super.beforeActivityLaunched();

        OkHttpClient mockHttp = create mock OkHttpClient

        MyDaggerComponent injector = DaggerMyDaggerComponent.
                builder().httpModule(new HttpModule(mockHttp)).build();

        MyApp app = (MyApp) InstrumentationRegistry.getInstrumentation().
                getTargetContext().getApplicationContext();

        app.externalInjectorInitialization(injector);

    }
};

然后我们以通常的方式进行测试:

代码语言:javascript
复制
@Test
public void testHttpRequest() throws IOException {
    onView(withId(R.id.btn_execute)).perform(click());

    onView(withId(R.id.tv_result))
            .check(matches(withText(EXPECTED_RESPONSE_BODY)));
}

上面的(模块)覆盖方法有效,但它需要为每个测试创建一个测试类,以便能够为每个测试提供单独的规则/(模拟设置)。我怀疑/猜测/希望有一种更简单、更优雅的方式。在那里吗?

这个方法很大程度上是基于@tomrozb for this question的答案。我只是添加了避免双重注入器创建的逻辑。

EN

回答 2

Stack Overflow用户

发布于 2016-03-04 01:24:59

1.注入过多依赖项

需要注意的两件事:

  1. 组件可以为自己提供
  2. 如果可以注入一次,则可以再次注入(并覆盖旧依赖项)

我所做的就是从我的测试用例中注入旧的依赖项。既然你的代码是干净的,所有的东西都被正确地限定了作用域,那么应该不会出什么差错,对吧?

下面的方法只有在你不依赖全局状态的情况下才会起作用,因为如果你在某个地方保留了对旧组件的引用,那么在运行时更改应用程序组件将不起作用。一旦您创建了下一个Activity,它将获取新的应用程序组件,并提供您的测试依赖项。

此方法依赖于对作用域的正确处理。完成并重新启动活动应该重新创建其依赖项。因此,您可以在没有活动运行时或在启动新活动之前切换应用程序组件。

在测试用例中,只需在需要时创建组件

代码语言:javascript
复制
// in @Test or @Before, just inject 'over' the old state
App app = (App) InstrumentationRegistry.getTargetContext().getApplicationContext();
AppComponent component = DaggerAppComponent.builder()
        .appModule(new AppModule(app))
        .build();
component.inject(app);

如果您有一个如下所示的应用程序...

代码语言:javascript
复制
public class App extends Application {

    @Inject
    AppComponent mComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        DaggerAppComponent.builder().appModule(new AppModule(this)).build().inject(this);
    }
}

...it将注入自身以及您在Application中定义的任何其他依赖项。随后的任何调用都将获得新的依赖项。

2.使用不同的配置和应用程序

您可以选择与您的指令插入测试一起使用的配置:

代码语言:javascript
复制
android {
...
    testBuildType "staging"
}

使用gradle资源合并,您可以选择为不同的构建类型使用多个不同版本的App

Application类从main源文件夹移动到debugrelease文件夹。Gradle将根据配置编译正确的源码集。然后,您可以根据需要修改应用程序的调试和发布版本。

如果您不希望使用不同的Application类进行调试和发布,则可以创建另一个buildType,仅用于您的检测测试。同样的原则也适用:将Application类复制到每个源集文件夹,否则将收到编译错误。由于您需要在debugrlease目录中拥有相同的类,因此可以创建另一个目录来包含用于调试和发布的类。然后将使用的目录添加到您的调试和发布源集。

票数 8
EN

Stack Overflow用户

发布于 2016-03-12 09:26:26

有一种更简单的方法可以做到这一点,即使是Dagger 2文档也提到了这一点,但它们并没有让它变得非常明显。以下是文档中的一段代码片段。

代码语言:javascript
复制
@Provides static Pump providePump(Thermosiphon pump) {
    return pump;
}

热虹吸器执行泵,只要需要泵,Dagger就会注入热虹吸管。

回到你的例子。您可以使用静态布尔数据成员创建一个Module,它允许您在真实和模拟测试对象之间动态切换,如下所示。

代码语言:javascript
复制
@Module
public class HttpModule {

    private static boolean isMockingHttp;

    public HttpModule() {}

    public static boolean mockHttp(boolean isMockingHttp) {
        HttpModule.isMockingHttp = isMockingHttp;
    }

    @Provides
    HttpClient providesHttpClient(OkHttpClient impl, MockHttpClient mockImpl) {
        return HttpModule.isMockingHttp ? mockImpl : impl;
    }

}

HttpClient可以是扩展的超类,也可以是由OkHttpClient和MockHttpClient实现的接口。Dagger将自动构造所需的类并注入它的内部依赖项,就像热虹吸管一样。

要模拟HttpClient,只需在将依赖项注入应用程序代码之前调用HttpModule.mockHttp(true)即可。

这种方法的好处是:

  • 不需要创建单独的测试组件,因为模拟是在模块中注入的level.
  • Application代码保持原始。
票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/35771356

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档