JobPlus知识库 IT 软件开发 文章
翻译Dagger 2与测试

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 { /* … */ }@Moduleclass 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. */@Moduleclass 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.}

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

¥ 打赏支持
376人赞 举报
分享到
用户评价(0)

暂无评价,你也可以发布评价哦:)

0 人收藏了这篇文章
腾讯云数据库性能卓越稳定可靠,为您解决数据库运维难题
广告
扫码APP

扫描使用APP

扫码使用

扫描使用小程序