这是在单元测试中使用Dagger 2 for Android app的正确方法来覆盖与模拟/假货的依赖关系吗?

Ogn*_*yan 19 android unit-testing dagger-2

对于"常规"Java项目来说,使用模拟/伪造的方法覆盖单元测试中的依赖项很容易.您必须简单地构建Dagger组件并将其提供给驱动应用程序的"main"类.

对于Android而言,事情并不那么简单,我已经搜索了很长时间才能找到合适的例子,但是我无法找到,所以我必须创建自己的实现,我真的很感激反馈这是使用Dagger 2或者那里的正确方法是一种更简单/更优雅的方式来覆盖依赖项.

这里的解释(项目源可以在github上找到):

鉴于我们有一个简单的应用程序使用Dagger 2与单个模块的单个匕首组件我们想要创建使用JUnit4,MockitoEspresso的 Android单元测试:

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

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

    ...
Run Code Online (Sandbox Code Playgroud)

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

externalInjectorInitialization()方法可以通过单元测试来调用,以便set从外部源进行注入,即单元测试.

到现在为止还挺好.

让我们看看单元测试方面的内容如何:

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

public class MyTestApp extends MyApp {
    @Override
    protected void initInjector() {
        // empty
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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);
    }
}
Run Code Online (Sandbox Code Playgroud)

... newApplication()我们在哪里有效地用测试类替换原来的app类.

然后我们必须告诉测试框架我们有并且想要使用我们的自定义测试运行器,所以我们在build.gradle中添加:

defaultConfig {
    ...
    testInstrumentationRunner 'com.bolyartech.d2overrides.utils.MyTestRunner'
    ...
}
Run Code Online (Sandbox Code Playgroud)

当进行单元测试时,我们的原件MyApp被替换为MyTestApp.现在我们必须为应用程序创建并向我们的组件/注入器提供模拟/假动作externalInjectorInitialization().为此,我们扩展了正常的ActivityTestRule:

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

    }
};
Run Code Online (Sandbox Code Playgroud)

然后我们按常规方式进行测试:

@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)));
}
Run Code Online (Sandbox Code Playgroud)

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

这个方法主要基于@tomrozb对这个问题的回答.我刚刚添加了逻辑以避免双喷射器创建.

Dav*_*jak 8

1.注入依赖项

有两点需要注意:

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

我要做的就是刚刚注入我的测试情况下,旧的依赖关系.由于你的代码很干净,所有东西都是正确的,所以不应该出错 - 对吧?

以下内容仅在您不依赖Global State时才有效,因为如果您在某个地方保留对旧组件的引用,则在运行时更改应用程序组件将不起作用.一旦您创建了下一个Activity,它将获取新的应用程序组件,并将提供您的测试依赖项.

此方法取决于范围的正确处理.完成并重新启动活动应重新创建其依赖项.因此,您可以在没有活动运行时或在开始新活动之前切换应用程序组件.

在您的测试用例中,只需根据需要创建组件即可

// 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);
Run Code Online (Sandbox Code Playgroud)

如果您有类似以下的应用程序......

public class App extends Application {

    @Inject
    AppComponent mComponent;

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

...它将注入自己以及您在其中定义的任何其他依赖项Application.然后,任何后续调用都将获得新的依赖项.


2.使用其他配置和应用程序

您可以选择要与仪器测试一起使用的配置:

android {
...
    testBuildType "staging"
}
Run Code Online (Sandbox Code Playgroud)

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

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

如果您不想Application为调试和发布创建不同的类,则可以创建另一个类buildType,仅用于仪器测试.同样的原则适用:将Application类复制到每个源集文件夹,否则您将收到编译错误.因为您需要在debugand rlease目录中拥有相同的类,所以可以创建另一个目录来包含用于调试和发布的类.然后添加用于调试和发布源集的目录.