如何使用Dagger 2.0覆盖单元测试中的模块/依赖项?

G. *_*ard 56 android dagger dagger-2

我有一个简单的Android活动,只有一个依赖项.我将依赖注入到活动中,onCreate如下所示:

Dagger_HelloComponent.builder()
    .helloModule(new HelloModule(this))
    .build()
    .initialize(this);
Run Code Online (Sandbox Code Playgroud)

在我的ActivityUnitTestCase我要重写一个模拟的Mockito的依赖.我假设我需要使用提供模拟的特定于测试的模块,但我无法弄清楚如何将此模块添加到对象图中.

在Dagger 1.x中,这显然是用这样的东西完成的:

@Before
public void setUp() {
  ObjectGraph.create(new TestModule()).inject(this);
}
Run Code Online (Sandbox Code Playgroud)

什么是Dagger 2.0相当于以上?

您可以在GitHub上看到我的项目及其单元测试.

tom*_*ozb 48

可能这更像是对测试模块覆盖的适当支持的解决方法,但它允许使用测试模块覆盖生产模块.下面的代码片段显示了只有一个组件和一个模块的简单情况,但这应适用于任何方案.它需要大量的样板和代码重复,所以要注意这一点.我相信将来会有更好的方法来实现这一目标.

我还创建了一个包含Espresso和Robolectric示例项目.此答案基于项目中包含的代码.

解决方案需要两件事:

  • 为...提供额外的制定者 @Component
  • 测试组件必须扩展生产组件

假设我们简单Application如下:

public class App extends Application {

    private AppComponent mAppComponent;

    @Override
    public void onCreate() {
        super.onCreate();
        mAppComponent = DaggerApp_AppComponent.create();
    }

    public AppComponent component() {
        return mAppComponent;
    }

    @Singleton
    @Component(modules = StringHolderModule.class)
    public interface AppComponent {

        void inject(MainActivity activity);
    }

    @Module
    public static class StringHolderModule {

        @Provides
        StringHolder provideString() {
            return new StringHolder("Release string");
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我们要在App课堂上添加额外的方法.这允许我们替换生产组件.

/**
 * Visible only for testing purposes.
 */
// @VisibleForTesting
public void setTestComponent(AppComponent appComponent) {
    mAppComponent = appComponent;
}
Run Code Online (Sandbox Code Playgroud)

如您所见,该StringHolder对象包含"Release string"值.这个对象被注入了MainActivity.

public class MainActivity extends ActionBarActivity {

    @Inject
    StringHolder mStringHolder;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        ((App) getApplication()).component().inject(this);
    }
}
Run Code Online (Sandbox Code Playgroud)

在我们的测试中,我们希望提供StringHolder"测试字符串".我们要在创建App之前在类中设置测试组件MainActivity- 因为它StringHolder是在onCreate回调中注入的.

在Dagger v2.0.0中,组件可以扩展其他接口.我们可以利用它来创建我们的TestAppComponent哪些扩展 AppComponent.

@Component(modules = TestStringHolderModule.class)
interface TestAppComponent extends AppComponent {

}
Run Code Online (Sandbox Code Playgroud)

现在我们可以定义我们的测试模块,例如TestStringHolderModule.最后一步是使用先前在App类中添加的setter方法设置测试组件.在创建活动之前执行此操作非常重要.

((App) application).setTestComponent(mTestAppComponent);
Run Code Online (Sandbox Code Playgroud)

浓咖啡

对于Espresso,我创建了自定义ActivityTestRule,允许在创建活动之前交换组件.你可以在DaggerActivityTestRule 这里找到代码.

使用Espresso进行样品测试:

@RunWith(AndroidJUnit4.class)
@LargeTest
public class MainActivityEspressoTest {

    public static final String TEST_STRING = "Test string";

    private TestAppComponent mTestAppComponent;

    @Rule
    public ActivityTestRule<MainActivity> mActivityRule =
            new DaggerActivityTestRule<>(MainActivity.class, new OnBeforeActivityLaunchedListener<MainActivity>() {
                @Override
                public void beforeActivityLaunched(@NonNull Application application, @NonNull MainActivity activity) {
                    mTestAppComponent = DaggerMainActivityEspressoTest_TestAppComponent.create();
                    ((App) application).setTestComponent(mTestAppComponent);
                }
            });

    @Component(modules = TestStringHolderModule.class)
    interface TestAppComponent extends AppComponent {

    }

    @Module
    static class TestStringHolderModule {

        @Provides
        StringHolder provideString() {
            return new StringHolder(TEST_STRING);
        }
    }

    @Test
    public void checkSomething() {
        // given
        ...

        // when
        onView(...)

        // then
        onView(...)
                .check(...);
    }
}
Run Code Online (Sandbox Code Playgroud)

Robolectric

Robolectric更容易归功于它RuntimeEnvironment.application.

使用Robolectric进行样品测试:

@RunWith(RobolectricGradleTestRunner.class)
@Config(emulateSdk = 21, reportSdk = 21, constants = BuildConfig.class)
public class MainActivityRobolectricTest {

    public static final String TEST_STRING = "Test string";

    @Before
    public void setTestComponent() {
        AppComponent appComponent = DaggerMainActivityRobolectricTest_TestAppComponent.create();
        ((App) RuntimeEnvironment.application).setTestComponent(appComponent);
    }

    @Component(modules = TestStringHolderModule.class)
    interface TestAppComponent extends AppComponent {

    }

    @Module
    static class TestStringHolderModule {

        @Provides
        StringHolder provideString() {
            return new StringHolder(TEST_STRING);
        }
    }

    @Test
    public void checkSomething() {
        // given
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);

        // when
        ...

        // then
        assertThat(...)
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 唯一的“问题”是我无法扩展StringHolderModule。因此,如果我的模块有很多提供程序,则只需要复制/粘贴并模拟我想要的那个。除此之外,这个答案很酷!/ o / (2认同)
  • @Tsuharesu"并且只模拟我想要的那个" - 也许你应该解耦你的模块?将所有提供方法保存在一个模块中是一种"难闻的气味".它可以防止代码重用.根据不同类型的依赖项创建一个模块,您将轻松摆脱这个问题. (2认同)
  • 没关系,但是你在`Application`类中添加一个公共setter只是为了测试目的......这也是你的生产代码,你只是为了测试而公开的东西.Dagger 2还没有测试支持,这就是为什么我们都试图找到最干净的解决方法.在Google修复它之前,我会坚持使用Dagger 1. (2认同)

vau*_*oid 24

正如@EpicPandaForce正确地说,你无法扩展模块.然而,我想出了一个偷偷摸摸的解决方法,我认为避免了其他例子遭受的许多样板.

"扩展"模块的技巧是创建部分模拟,并模拟您要覆盖的提供者方法.

使用Mockito:

MyModule module = Mockito.spy(new MyModule());
Mockito.doReturn("mocked string").when(module).provideString();

MyComponent component = DaggerMyComponent.builder()
        .myModule(module)
        .build();

app.setComponent(component);
Run Code Online (Sandbox Code Playgroud)

我在这里创建了这个要点以展示一个完整的例子.

编辑

事实证明,即使没有部分模拟,你也可以做到这一点,如下所示:

MyComponent component = DaggerMyComponent.builder()
        .myModule(new MyModule() {
            @Override public String provideString() {
                return "mocked string";
            }
        })
        .build();

app.setComponent(component);
Run Code Online (Sandbox Code Playgroud)


yuv*_*val 10

@tomrozb提出的解决方法非常好,让我走上正轨,但我的问题是它暴露了setTestComponent()PRODUCTION Application类中的一个方法.我能够使这个工作略有不同,这样我的生产应用程序就不需要了解我的测试环境.

TL; DR - 使用测试组件和模块的测试应用程序扩展Application类.然后创建一个在测试应用程序而不是生产应用程序上运行的自定义测试运行器.


编辑:此方法仅适用于全局依赖项(通常标记为@Singleton).如果您的应用程序具有不同范围的组件(例如,每个活动),那么您需要为每个范围创建子类,或者使用@ tomrozb的原始答案.感谢@tomrozb指出这一点!


此示例使用AndroidJUnitRunner测试运行器,但这可能适用于Robolectric和其他人.

首先,我的生产应用程序.它看起来像这样:

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

这样,我的活动和其他使用的类@Inject只需调用类似于getApp().getComponent().inject(this);将自己注入依赖图的东西.

为了完整性,这是我的组件:

@Singleton
@Component(modules = {MyModule.class})
public interface MyComponent {
    void inject(MyApp app);
    // other injects and getters
}
Run Code Online (Sandbox Code Playgroud)

我的模块:

@Module
public class MyModule {
    // EDIT: This solution only works for global dependencies
    @Provides @Singleton
    public MyClass provideMyClass() { ... }

    // ... other providers
}
Run Code Online (Sandbox Code Playgroud)

对于测试环境,请从生产组件中扩展测试组件.这与@ tomrozb的答案相同.

@Singleton
@Component(modules = {MyTestModule.class})
public interface MyTestComponent extends MyComponent {
    // more component methods if necessary
}
Run Code Online (Sandbox Code Playgroud)

测试模块可以是你想要的任何东西.大概你会在这里处理你的嘲笑和事情(我使用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.
}
Run Code Online (Sandbox Code Playgroud)

所以,现在,棘手的部分.创建一个从生产应用程序类扩展的测试应用程序类,并覆盖该setComponent()方法以使用测试模块设置测试组件.请注意,这只有在MyTestComponent是后代的情况下才有效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)
    }
}
Run Code Online (Sandbox Code Playgroud)

确保setComponent()在开始测试之前调用应用程序以确保图表设置正确.像这样的东西:

@Before
public void setUp() {
    MyTestApp app = (MyTestApp) getInstrumentation().getTargetContext().getApplicationContext();
    app.setComponent()
    ((MyTestComponent) app.getComponent()).inject(this)
}
Run Code Online (Sandbox Code Playgroud)

最后,最后一个缺失的部分是使用自定义测试运行器覆盖TestRunner.在我的项目中,我正在使用AndroidJUnitRunner它,但看起来你可以用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);
    }
}
Run Code Online (Sandbox Code Playgroud)

您还必须更新您的testInstrumentationRunnergradle,如下所示:

testInstrumentationRunner "com.mypackage.TestRunner"
Run Code Online (Sandbox Code Playgroud)

如果您使用的是Android Studio,则还必须从运行菜单中单击"编辑配置",然后在"特定仪器运行器"下输入测试运行器的名称.

就是这样!希望这些信息有助于某人:)

  • 这是IMO目前最干净的方式(虽然我更喜欢覆盖`getComponent`而不是`setComponent`,甚至是私有的,但结果是一样的).*对于robolectric运行测试*,可以通过使用`@Config(application = MyTestApp.class)`注释测试类或方法来指定应用程序类. (2认同)
  • 在我的例子中,如果我将文件放在src/test下,则无法生成`DaggerMyTestComponent`.我必须使用生产代码将它放在src/main下.这真的很奇怪.有什么想法吗? (2认同)
  • @RotemSlootzky是的,解决方案是在您的gradle文件中添加`testApt'com.google.dagger:dagger-compiler:2.0.2'`。这样,apt进程也将适用于测试源代码。 (2认同)