官方文档链接:https://google.github.io/dagger/testing.html
1.前言
官网上还有篇关于Java中异步地依赖注入的文章,由于得引入Guava包,感觉Android上不太常用,所以没有翻译。若后期项目需要,会再来翻译的。
使用像Dagger之类的依赖注入框架的好处之一,是它让代码测试更简单。下面探讨一些测试Dagger构建的应用的方法。
2.单元测试不要使用Dagger
如果想要写个小的单元测试来测试@Inject
注解的类,其实不需要使用Dagger。仅需调用@Inject
注解的构造方法、设置@Inject
注解的属性和调用需测试的方法,如果可以,直接传递假的或模拟的依赖项。
final class ThingDoer {
private final ThingGetter getter;
private final ThingPutter putter;
@Inject ThingDoer(ThingGetter getter, ThingPutter putter) {
this.getter = getter;
this.putter = putter;
}
String doTheThing(int howManyTimes) { /* … */ }
}
public class ThingDoerTest {
@Test
public void testDoTheThing() {
ThingDoer doer = new ThingDoer(fakeGetter, fakePutter);
assertEquals("done", doer.doTheThing(5));
}
}
3.替换依赖数据
功能、集成、端到端测试通常用于产线应用,用假的(在大型功能测试中不使用模拟的)数据替换持久化、后端和认证系统的数据,使应用的剩余部分能正常工作。这种方法在测试配置替换产品配置中的一些数据时,有助于掌控一个(也许少量的)测试配置项。
选项1:通过子类Module重写依赖项(不建议)
在测试Component中,替换依赖项最简单的办法就是通过子类重写Module里@Provides
注解的方法。(后面会讲到存在的问题。)当创建Component的实例,传入它需使用的Module对象。(可以但不需要传入这样的Module对象,有无参构造方法或都是静态方法 。)这意味着可以传入那些Module子类的对象,而且那些子类可以重写一些@Provides
注解的方法来替换依赖项。
@Component(modules = {AuthModule.class, /* … */})
interface MyApplicationComponent { /* … */ }
@Module
class AuthModule {
@Provides AuthManager authManager(AuthManagerImpl impl) {
return impl;
}
}
class FakeAuthModule extends AuthModule {
@Override
AuthManager authManager(AuthManagerImpl impl) {
return new FakeAuthManager();
}
}
MyApplicationComponent testingComponent = DaggerMyApplicationComponent.builder()
.authModule(new FakeAuthModule())
.build();
但这种方法有些局限性:
第一,使用Module的子类不能改变依赖图内的关系:不能增加、删除或更改依赖。尤其是:
- 重写
@Provides
注解的方法不能更改它参数类型,且缩小范围的返回类型对Dagger而言并不影响依赖图。在上面的例子中,testingComponent对象需要的仍然是AuthManagerImpl和它相关的依赖,即使它们没有被使用。 - 同样的,重写Module不能给依赖图增加关系,包括新的多元绑定(即使仍然能重写
SET_VALUES
方法返回不同的Set)。子类中任何新的@Provides
注解的方法都默认被Dagger忽略。实际上,可理解为假的依赖项欺骗不了依赖注入。
第二,这种方式下,可重写的@Provides
注解的方法不可能是静态的,所以它们Module对象不能被忽略。
选项2:分开配置Component
另一种方法要求应用中有更多预设的Module。产线应用中的每个配置,都得在测试Component中进行不同的配置。测试Component类继承自产线Component类,而且添加一系列不同的Module。
@Component(modules = {
OAuthModule.class, // real auth
FooServiceModule.class, // real backend
OtherApplicationModule.class,
/* … */ })
interface ProductionComponent {
Server server();
}
@Component(modules = {
FakeAuthModule.class, // fake auth
FakeFooServiceModule.class, // fake backend
OtherApplicationModule.class,
/* … */})
interface TestComponent extends ProductionComponent {
FakeAuthManager fakeAuthManager();
FakeFooService fakeFooService();
}
测试时,调用
DaggerTestComponent.builder()
取代DaggerProductionComponent.builder()
作为Main方法。注意,测试Component接口可以增加预定的对假数据的处理(fakeAuthManager()
和fakeFooService()
),那样必要情况下,可在测试中访问它们来掌控数据。
下面来讲一讲如何设计Module来简化这个模式。
4.可测试的模块设计
Module类是一种工具类:包含单独的@Provides
注解的方法的集合,里面每个方法都可能被用来给应用注入需要的一些类型。(虽然几个@Provides
注解的方法可能相关联,一个依赖另一个提供的类型,它们通常不会显示调用彼此或依赖相同的可变状态。一些@Provides
注解的方法引用相同的属性对象,这样的话它们实际并不独立。这里给点建议,无论如何要像对待工具方法一样对待@Provides
注解的方法,因为它使Module在测试时更容易被替换。)
那么如何决定哪些@Provides
注解的方法应该放在一个Module类中?
一方面考虑到将依赖划分为公开的和内部的,然后进一步考虑公开的依赖是否有合理的替代方案。
- 公开的依赖是那些提供功能的、被应用其它部分使用的。像AuthManager或User或DocDatabase这些类型是公开的:在Module中声明,应用其它部分可以使用它们。
- 内部的依赖是除公开依赖之外的:被用来实现一些公开的类型,除了作为它的一部分,并不一定要被使用。举个例子,配置认证客户端ID或OAuthKeyStore的依赖打算只在AuthManager实现认证的时候使用,而不是应用的其它部分。这些依赖通常是包内私有类型或被包内私有限定符修饰。
这些公开的依赖将有合理的替代方案,主要用于测试,其它情况则不用。举个例子,像AuthManager这类型的替代依赖项:一个用于测试,其它用于不同的认证/授权协议。
另一方面,如果AuthManager接口有个方法返回当前登录的用户,可能想要简单调用AuthManager的getCurrentUser()
方法提供User的公开依赖。这种公开的依赖不太可能需要替代方案。
一旦划分为带合理替代方案的公开依赖、不带合理替代方案的公开依赖和内部依赖,可以考虑这样安排它们到Module中:
- 为每个带合理替代方案的公开依赖提供Module。这Module显示包含一个公开的依赖,以及它需要的所有的内部依赖。
- 所有不带合理替代方案的公开依赖按照功能的顺序放入Module中。
- 每个公开依赖的Module应该包含需被提供公开依赖的不带合理替代方案的模块。
通过描述提供的公开依赖来记录每个Module是个好的主意。这有个认证相关的例子。有个AuthManager
接口及两个实现,一个实现有认证逻辑,另一个假的实现用于测试。产线配置将使用真实的Module,而测试配置假的Module。同上,还有个不期望随着配置改变的关于当前用户的显式依赖。
/**
* Provides auth bindings that will not change in different auth configurations,
* such as the current user.
*/
@Module
class AuthModule {
@Provides static User currentUser(AuthManager authManager) {
return authManager.currentUser();
}
// Other bindings that don’t differ among AuthManager implementations.
}
/** Provides a {@link AuthManager} that uses OAuth. */
@Module(includes = AuthModule.class) // Include no-alternative bindings.
class OAuthModule {
@Provides static AuthManager authManager(OAuthManager authManager) {
return authManager;
}
// Other bindings used only by OAuthManager.
}
/** Provides a fake {@link AuthManager} for testing. */
@Module(includes = AuthModule.class) // Include no-alternative bindings.
class FakeAuthModule {
@Provides static AuthManager authManager(FakeAuthManager authManager) {
return authManager;
}
// Other bindings used only by FakeAuthManager.
}
5.总结
关于Dagger 2的常见使用,到此算是翻译结束了。通过这段时间的学习,觉得Dagger 2对于代码的拆解和封装有很大的帮助,可以大大简化代码,突出体现业务逻辑,降低了应用的耦合性。欢迎大家在使用的同时,将心得体会与我交流,在此感谢!
网友评论