我有一个simple Android activity,只有一个依赖项。我将依赖项注入活动onCreate
,如下所示:
Dagger_HelloComponent.builder()
.helloModule(new HelloModule(this))
.build()
.initialize(this);
在我的ActivityUnitTestCase
中,我想用Mockito模拟覆盖依赖项。我假设我需要使用一个提供模拟的特定于测试的模块,但我无法弄清楚如何将该模块添加到对象图中。
在Dagger 1.x中,这显然是用something like this完成的:
@Before
public void setUp() {
ObjectGraph.create(new TestModule()).inject(this);
}
Dagger 2.0与上述相同的是什么?
您可以看到我的项目及其单元测试here on GitHub。
答案 0 :(得分:46)
这可能是对测试模块覆盖的适当支持的更多解决方法,但它允许使用测试模块覆盖生产模块。下面的代码片段显示了只有一个组件和一个模块的简单情况,但这应适用于任何方案。它需要大量的样板和代码重复,所以要注意这一点。我确信将来有更好的方法来实现这一目标。
我还创建了project with examples for Espresso and 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");
}
}
}
我们要为App
课程添加其他方法。这允许我们替换生产组件。
/**
* Visible only for testing purposes.
*/
// @VisibleForTesting
public void setTestComponent(AppComponent appComponent) {
mAppComponent = appComponent;
}
正如您所见,StringHolder
对象包含"发布字符串"值。此对象将注入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);
}
}
在我们的测试中,我们希望为StringHolder
提供"测试字符串"。我们必须在创建App
之前在MainActivity
类中设置测试组件,因为StringHolder
已在onCreate
回调中注入。
在Dagger v2.0.0中,组件可以扩展其他接口。我们可以利用此功能创建我们的TestAppComponent
扩展 AppComponent
。
@Component(modules = TestStringHolderModule.class)
interface TestAppComponent extends AppComponent {
}
现在我们可以定义我们的测试模块,例如TestStringHolderModule
。最后一步是使用先前在App
类中添加的setter方法设置测试组件。在创建活动之前执行此操作非常重要。
((App) application).setTestComponent(mTestAppComponent);
<强>咖啡强>
对于Espresso,我创建了自定义ActivityTestRule
,允许在创建活动之前交换组件。您可以找到DaggerActivityTestRule
here的代码。
使用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(...);
}
}
<强> Robolectric 强>
由于RuntimeEnvironment.application
,使用Robolectric会更容易。
使用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(...)
}
}
答案 1 :(得分:22)
正如@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);
我在这里创建了this gist以显示完整的示例。
修改强>
事实证明,即使没有部分模拟,你也可以做到这一点,如下:
MyComponent component = DaggerMyComponent.builder()
.myModule(new MyModule() {
@Override public String provideString() {
return "mocked string";
}
})
.build();
app.setComponent(component);
答案 2 :(得分:9)
@tomrozb提出的解决方法非常好并且让我走上正轨,但我的问题是它在PRODUCTION setTestComponent()
类中暴露了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();
}
}
这样,我的活动和其他使用@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
}
对于测试环境,请从生产组件中扩展测试组件。这与@ tomrozb的答案相同。
@Singleton
@Component(modules = {MyTestModule.class})
public interface MyTestComponent extends MyComponent {
// more component methods if necessary
}
测试模块可以是你想要的任何东西。大概你会在这里处理你的嘲笑和事情(我使用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.
}
现在,这个棘手的部分。创建一个从生产应用程序类扩展的测试应用程序类,并覆盖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)
}
}
确保在开始测试之前在应用上调用setComponent()
,以确保图表设置正确。像这样:
@Before
public void setUp() {
MyTestApp app = (MyTestApp) getInstrumentation().getTargetContext().getApplicationContext();
app.setComponent()
((MyTestComponent) app.getComponent()).inject(this)
}
最后,最后一个缺失的部分是使用自定义测试运行器覆盖TestRunner。在我的项目中,我使用的是AndroidJUnitRunner
,但看起来你可以do the same with 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,则还必须从运行菜单中单击“编辑配置”,然后在“特定仪器运行器”下输入测试运行器的名称。
就是这样!希望这些信息有助于某人:)
答案 3 :(得分:2)
似乎我找到了另一种方式,到目前为止它已经发挥作用了。
首先,组件接口本身不是组件:
<强> MyComponent.java 强>
interface MyComponent {
Foo provideFoo();
}
然后我们有两个不同的模块:实际模块和测试模块。
<强> 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();
在测试代码中,我们使用:
TestModule testModule = new TestModule();
testModule.setFoo(someMockFoo);
MyComponent component = DaggerMyTestComponent.builder()
.testModule(testModule).build();
<...>
Foo foo = component.getFoo(); // will return someMockFoo
问题是我们必须将MyModule的所有方法复制到TestModule中,但是可以通过在MyModule中使用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;
}
}
答案 4 :(得分:1)
这个答案已经过时了。在编辑中阅读。
令人失望的是,无法从模块扩展,否则您将收到以下编译错误:
Error:(24, 21) error: @Provides methods may not override another method.
Overrides: Provides
retrofit.Endpoint hu.mycompany.injection.modules.application.domain.networking.EndpointModule.myServerEndpoint()
这意味着您不能只扩展“模拟模块”并替换原始模块。不,这并不容易。考虑到你设计组件的方式是它按类直接绑定模块,你也不能真正只做一个“TestComponent”,因为这意味着你必须重新发明一切划伤,你必须为每个变化做一个组件!显然,这不是一种选择。
因此,在较小的范围内,我最终做的是创建一个“提供者”,我给模块,它决定了我选择模拟还是生产类型。
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的答案。
答案 5 :(得分:0)
你们可以查看我的解决方案,我已经包含了子组件示例:https://github.com/nongdenchet/android-mvvm-with-tests。谢谢你@vaughandroid,我借用了你最重要的方法。以下是要点:
我创建了一个类来创建子组件。我的自定义应用程序还将包含此类的实例:
// 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类。此类包含两个替换根组件和构建器的方法:
public class TestApplication extends MyApplication {
public void setComponent(AppComponent appComponent) {
this.mAppComponent = appComponent;
}
public void setComponentBuilder(ComponentBuilder componentBuilder) {
this.mComponentBuilder = componentBuilder;
}
}
最后,我将尝试模拟或存根模块和构建器的依赖关系,以便为活动提供虚假依赖:
@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());
}
答案 6 :(得分:0)
我对机器人3。+ 有解决方案。
我有MainActivity,我想在创建时不进行注入就对其进行测试:
public class MainActivity extends BaseActivity{
@Inject
public Configuration configuration;
@Inject
public AppStateService appStateService;
@Inject
public LoginService loginService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.processIntent(getIntent()); // this is point where pass info from test
super.onCreate(savedInstanceState)
...
}
...
}
接下来我的BaseActivty:
public class BaseActivity extends AppCompatActivity {
protected Logger mLog;
protected boolean isTestingSession = false; //info about test session
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
if (!isTestingSession) { // check if it is in test session, if not enable injectig
AndroidInjection.inject(this);
}
super.onCreate(savedInstanceState);
}
// method for receive intent from child and scaning if has item TESTING with true
protected void processIntent(Intent intent) {
if (intent != null && intent.getExtras() != null) {
isTestingSession = intent.getExtras().getBoolean("TESTING", false);
}
}
最后是我的测试班:
@Before
public void setUp() throws Exception {
...
// init mocks...
loginServiceMock = mock(LoginService.class);
locServiceMock = mock(LocationClientService.class);
fakeConfiguration = new ConfigurationUtils(new ConfigurationXmlParser());
fakeConfiguration.save(FAKE_XML_CONFIGURATION);
appStateService = new AppStateService(fakeConfiguration, locServiceMock, RuntimeEnvironment.application);
// prepare activity
Intent intent = new Intent(RuntimeEnvironment.application, MainActivity.class);
intent.putExtra("TESTING", true);
ActivityController<MainActivity> activityController = Robolectric.buildActivity(MainActivity.class, intent); // place to put bundle with extras
// get the activity instance
mainActivity = activityController.get();
// init fields which should be injected
mainActivity.appStateService = appStateService;
mainActivity.loginService = loginServiceMock;
mainActivity.configuration = fakeConfiguration;
// and whoala
// now setup your activity after mock injection
activityController.setup();
// get views etc..
actionButton = mainActivity.findViewById(R.id.mainButtonAction);
NavigationView navigationView = mainActivity.findViewById(R.id.nav_view);
....
}
答案 7 :(得分:-5)
使用Dagger2,您可以使用生成的构建器API将特定模块(TestModule)传递给组件。
ApplicationComponent appComponent = Dagger_ApplicationComponent.builder()
.helloModule(new TestModule())
.build();
请注意,Dagger_ApplicationComponent是一个带有新@Component注释的生成类。