在Dagger 2.0的unit testing中如何覆盖模块/依赖项?

我有一个简单的Android活动 ,只有一个依赖。 我将dependency injection到活动的onCreate如下所示:

 Dagger_HelloComponent.builder() .helloModule(new HelloModule(this)) .build() .initialize(this); 

在我的ActivityUnitTestCase我想用Mockito模拟来覆盖依赖关系。 我假设我需要使用提供模拟的特定于testing的模块,但我无法弄清楚如何将此模块添加到对象图中。

在Dagger 1.x中,显然是这样做的:

 @Before public void setUp() { ObjectGraph.create(new TestModule()).inject(this); } 

什么是匕首2.0相当于上述?

你可以在GitHub上看到我的项目和它的unit testing。

可能这是一个解决scheme,更适合支持testing模块覆盖,但是它允许用testing模块覆盖生产模块。 下面的代码片断显示了只有一个组件和一个模块的简单情况,但是这适用于任何情况。 它需要大量的样板和代码重复,所以要注意这一点。 我相信未来会有更好的方法来实现这一点。

我还用Espresso和Robolectric的例子创build了一个项目 。 这个答案是基于项目中包含的代码。

解决scheme需要两件事情:

  • @Component提供额外的setter
  • testing组件必须扩展生产组件

假设我们有如下简单的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"); } } } 

我们必须为App类添加额外的方法。 这使我们能够更换生产组件。

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

正如你所看到的StringHolder对象包含“释放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); } } 

在我们的testing中,我们要提供“testingstring”的StringHolder 。 我们必须在创buildMainActivity之前在App类中设置testing组件 – 因为StringHolder是在onCreatecallback中注入的。

在Dagger v2.0.0组件中可以扩展其他接口。 我们可以利用这个来创build扩展 AppComponent

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

现在我们可以定义我们的testing模块,例如TestStringHolderModule 。 最后一步是使用App类中以前添加的setter方法设置testing组件。 在创build活动之前做到这一点很重要。

 ((App) application).setTestComponent(mTestAppComponent); 

浓咖啡

对于Espresso,我创build了自定义的ActivityTestRule ,它允许在创buildActivityTestRule之前交换组件。 你可以在这里findDaggerActivityTestRule代码。

用Espresso进行样品testing:

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

Robolectric

Robolectric感谢RuntimeEnvironment.application这更容易。

Robolectric的样品testing:

 @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(...) } } 

正如@EpicPandaForce所说的,你不能扩展模块。 不过,我想出了一个偷偷摸摸的解决办法,我认为避免了其他例子遭受的很多样板。

“扩展”一个模块的技巧是创build一个部分模拟,并嘲笑你想重写的提供者方法。

使用Mockito :

 MyModule module = Mockito.spy(new MyModule()); Mockito.doReturn("mocked string").when(module).provideString(); MyComponent component = DaggerMyComponent.builder() .myModule(module) .build(); app.setComponent(component); 

我在这里创造了这个要点来展示一个完整的例子。

编辑

事实certificate,即使没有部分模拟,你也可以这样做:

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

@tomrozb提出的解决方法非常好,并把我放在正确的轨道,但我的问题是它暴露了PRODUCTION Application类中的setTestComponent()方法。 我能够稍微改变这一点,这样我的生产应用程序就不必知道我的testing环境。

TL; DR – 扩展您的应用程序类与testing应用程序,使用您的testing组件和模块。 然后创build一个在testing应用程序上运行的自定义testing运行器,而不是生产应用程序。


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


这个例子使用AndroidJUnitRunnertesting运行器,但是这可能适用于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(); } } 

这样,我的活动和使用@Inject其他类只需调用getApp().getComponent().inject(this); 将自己注入到依赖关系图中。

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

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

和我的模块:

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

对于testing环境,从生产组件扩展testing组件。 这和@ tomrozb的回答是一样的。

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

而testing模块可以是任何你想要的。 大概你会在这里处理你的嘲笑和东西(我使用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. } 

所以现在,棘手的部分。 创build一个从您的生产应用程序类扩展的testing应用程序类,并覆盖setComponent()方法以使用testing模块设置testing组件。 请注意,只有MyTestComponentMyComponent的后代才能使用。

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

确保在开始testing之前,先在应用程序上调用setComponent() ,以确保graphics设置正确。 像这样的东西:

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

最后,最后一个缺失的部分是用自定义testing运行器覆盖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); } } 

您还必须更新testInstrumentationRunner gradle,如下所示:

 testInstrumentationRunner "com.mypackage.TestRunner" 

如果您使用的是Android Studio,则还必须从运行菜单中单击编辑configuration,然后在“特定仪器运行器”下input您的testing运行器的名称。

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

看来我发现了另一种方式,迄今为止工作。

首先,一个组件接口本身不是一个组件:

MyComponent.java

 interface MyComponent { Foo provideFoo(); } 

然后我们有两个不同的模块:实际模块和testing模块。

MyModule.java

 @Module class MyModule { @Provides public Foo getFoo() { return new Foo(); } } 

TestModule.java

 @Module class TestModule { private Foo foo; public void setFoo(Foo foo) { this.foo = foo; } @Provides public Foo getFoo() { return foo; } } 

我们有两个组件来使用这两个模块:

MyRealComponent.java

 @Component(modules=MyModule.class) interface MyRealComponent extends MyComponent { Foo provideFoo(); // without this dagger will not do its magic } 

MyTestComponent.java

 @Component(modules=TestModule.class) interface MyTestComponent extends MyComponent { Foo provideFoo(); } 

在应用程序中我们这样做:

 MyComponent component = DaggerMyRealComponent.create(); <...> Foo foo = component.getFoo(); 

在testing代​​码中,我们使用:

 TestModule testModule = new TestModule(); testModule.setFoo(someMockFoo); MyComponent component = DaggerMyTestComponent.builder() .testModule(testModule).build(); <...> Foo foo = component.getFoo(); // will return someMockFoo 

问题是我们必须将MyModule的所有方法复制到TestModule中,但是可以通过在TestModule中使用MyModule并使用MyModule的方法来完成,除非它们是从外部直接设置的。 喜欢这个:

TestModule.java

 @Module class TestModule { MyModule myModule = new MyModule(); private Foo foo = myModule.getFoo(); public void setFoo(Foo foo) { this.foo = foo; } @Provides public Foo getFoo() { return foo; } } 

这个答案已经过时了。 阅读下面的编辑。

令人失望的是,你不能从一个模块扩展 ,否则你会得到以下编译错误:

 Error:(24, 21) error: @Provides methods may not override another method. Overrides: Provides retrofit.Endpoint hu.mycompany.injection.modules.application.domain.networking.EndpointModule.mySe‌​rverEndpoint() 

这意味着你不能只扩展一个“模拟模块”并replace原来的模块。 不,不是那么容易。 考虑到你devise你的组件的方式,它直接绑定模块的类,你不能真的只是一个“TestComponent”,因为这意味着你必须从头开始重新创build一切 ,你会有为每一个变化做一个组件! 显然这不是一个选项。

因此,在规模较小的情况下,我最终做了一个“提供者”,我给了模块,它决定了我select模拟还是生产types。

 public interface EndpointProvider { Endpoint serverEndpoint(); } public class ProdEndpointProvider implements EndpointProvider { @Override public Endpoint serverEndpoint() { return new ServerEndpoint(); } } public class TestEndpointProvider implements EndpointProvider { @Override public Endpoint serverEndpoint() { return new TestServerEndpoint(); } } @Module public class EndpointModule { private Endpoint serverEndpoint; private EndpointProvider endpointProvider; public EndpointModule(EndpointProvider endpointProvider) { this.endpointProvider = endpointProvider; } @Named("server") @Provides public Endpoint serverEndpoint() { return endpointProvider.serverEndpoint(); } } 

编辑:显然,如错误消息所示,您不能覆盖使用@Provides注释方法的另一种方法,但这并不意味着你不能重写@Provides注释的方法:(

所有的魔法都是徒劳的! 你可以扩展一个模块,而不用把@Provides放在这个方法上,它可以工作…参考@vaughandroid的答案。

你们可以看看我的解决scheme,我已经包含子组件示例: https : //github.com/nongdenchet/android-mvvm-with-tests 。 谢谢@vaughandroid,我借用了你的压倒一切的方法。 这里是主要观点:

  1. 我创build一个类来创build子组件。 我的自定义应用程序也将保存这个类的一个实例:

     // The builder class public class ComponentBuilder { private AppComponent appComponent; public ComponentBuilder(AppComponent appComponent) { this.appComponent = appComponent; } public PlacesComponent placesComponent() { return appComponent.plus(new PlacesModule()); } public PurchaseComponent purchaseComponent() { return appComponent.plus(new PurchaseModule()); } } // My custom application class public class MyApplication extends Application { protected AppComponent mAppComponent; protected ComponentBuilder mComponentBuilder; @Override public void onCreate() { super.onCreate(); // Create app component mAppComponent = DaggerAppComponent.builder() .appModule(new AppModule()) .build(); // Create component builder mComponentBuilder = new ComponentBuilder(mAppComponent); } public AppComponent component() { return mAppComponent; } public ComponentBuilder builder() { return mComponentBuilder; } } // Sample using builder class: public class PurchaseActivity extends BaseActivity { ... @Override protected void onCreate(Bundle savedInstanceState) { ... // Setup dependency ((MyApplication) getApplication()) .builder() .purchaseComponent() .inject(this); ... } } 
  2. 我有一个自定义的TestApplication扩展了上面的MyApplication类。 这个类包含两个方法来replace根组件和构build器:

     public class TestApplication extends MyApplication { public void setComponent(AppComponent appComponent) { this.mAppComponent = appComponent; } public void setComponentBuilder(ComponentBuilder componentBuilder) { this.mComponentBuilder = componentBuilder; } } 
  3. 最后,我将尝试模拟或存根模块和构build器的依赖关系来为活动提供伪依赖:

     @MediumTest @RunWith(AndroidJUnit4.class) public class PurchaseActivityTest { @Rule public ActivityTestRule<PurchaseActivity> activityTestRule = new ActivityTestRule<>(PurchaseActivity.class, true, false); @Before public void setUp() throws Exception { PurchaseModule stubModule = new PurchaseModule() { @Provides @ViewScope public IPurchaseViewModel providePurchaseViewModel(IPurchaseApi purchaseApi) { return new StubPurchaseViewModel(); } }; // Setup test component AppComponent component = ApplicationUtils.application().component(); ApplicationUtils.application().setComponentBuilder(new ComponentBuilder(component) { @Override public PurchaseComponent purchaseComponent() { return component.plus(stubModule); } }); // Run the activity activityTestRule.launchActivity(new Intent()); } 

使用Dagger2,您可以使用生成的构build器API将特定模块(TestModule)传递给组件。

 ApplicationComponent appComponent = Dagger_ApplicationComponent.builder() .helloModule(new TestModule()) .build(); 

请注意,Dagger_ApplicationComponent是一个带有新的@Component注解的生成类。