在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
是在onCreate
callback中注入的。
在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组件。 请注意,只有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) } }
确保在开始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.myServerEndpoint()
这意味着你不能只扩展一个“模拟模块”并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,我借用了你的压倒一切的方法。 这里是主要观点:
-
我创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); ... } }
-
我有一个自定义的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; } }
-
最后,我将尝试模拟或存根模块和构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注解的生成类。