如何使用 Dagger 编写测试

使用像 Dagger 这样的依赖注入框架的一个好处是可以让你更容易编写测试代码。本文档探讨了测试使用 Dagger 构建的应用程序的一些策略。

替换功能/集成/端到端测试的绑定

功能、集成、端到端测试通常使用生产应用程序,但是使用伪造的对象(不要在大型​​功能测试中使用模拟!) 来替代持久化、后端或认证系统,让应用程序的其余部分正常运行。

方法1:通过子类化模块(modules) 替换绑定 (不要这样做!)

替换测试组件中的绑定的最简单方法是覆盖子类中的模块的 @Provides 方法。 (但请看下面的问题。)

创建 Dagger 组件的实例时,会传入它使用的模块的实例。(你不必为没有arg构造函数的模块或没有实例方法的模块传递实例,但你可以。)这意味着你可以传递这些模块的子类的实例,并且这些子类可以覆盖某些 @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();

但这种方法存在局限性:

  • 使用模块子类不能更改绑定图的静态形状:它不能添加或删除绑定,也不能更改绑定的依赖关系。特别的:
    • 覆盖 @Provides 方法不能更改其参数类型,并且缩小返回类型对绑定图没有影响,因为 Dagger 理解它。在上面的示例中,testingComponent 仍然需要绑定 AuthManagerImpl 及其所有依赖项,即使它们未被使用。
    • 同样,覆盖模块无法向图形添加绑定,包括新的 multibinding(虽然你仍可以覆盖一个 SET_VALUES 方法来返回一个不同的集合)。Dagger 会默默忽略子类中的任何新 @Provides 方法。实际上,这意味着你的伪造对象无法利用依赖注入。
  • @Provides 以这种方式可覆盖的方法不能是静态的,因此不能省略它们的模块实例。

方法2:单独的组件配置

另一种方法需要在应用程序中更多地预先设计模块,应用程序的每个配置(生产和测试)都使用不同的组件配置。测试组件类型继承了生产组件类型并安装了一组不同的模块。

@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()。请注意,测试组件接口可以向伪实例(fakeAuthManager()fakeFooService())添加额外方法,以便测试代码可以在必要时访问它们来实现控制。

但是,如何设计模块以简化这种模式?

组织模块以实现可测试性

模块类是一种实用类:一个独立的 @Providers 方法的收集器,每一个模块类都可能被注射器使用来提供某种类型给应用程序。

虽然几个 @Provides 方法可能相关,因为它们依赖于另一个提供的类型,但它们通常不会明确地相互调用或依赖于相同的可变状态。一些 @Provides 方法确实引用相同的实例字段,在这种情况下它们实际上不是独立的。这里给出的建议是将 @Provides 方法视为实用方法,这样模块就可以很容易地被替代从而方便测试。

那么如何确定哪些 @Provides 方法应该合并到一个模块类?

考虑它的一种方法是将绑定分类为已发布的绑定和内部绑定,然后进一步确定哪些已发布的绑定具有合理的替代方案。

已发布的绑定是提供应用程序其他部分使用的功能的绑定。例如 AuthManagerUserDocDatabase 等类型是已发布类:它们绑定在一个模块中,以便应用程序的其余部分可以使用它们。

内部绑定是除已发布之外的绑定:在某些已发布类型的实现中使用的绑定,并且除此之外不被其他地方使用。例如,OAuth客户端 ID 配置的绑定或仅供AuthManagerOAuth 实现使用而不被程序其他部分使用的 OAuthKeyStore。这些绑定通常用于包私有类型,或者使用包私有限定符进行限定。

一些已发布的绑定将具有合理的替代方案,尤其是用于测试,而其他绑定则不会。例如,对于类似AuthManager 的类型,可能存在替代绑定:一个用于测试,另一个用于不同的身份验证/授权协议。

但另一方面,如果 AuthManager 接口有一个返回当前登录用户的方法,您可能希望通过在 AuthManager 上调用 getCurrentUser() 来发布为User提供的绑定。这种发布的绑定不太可能需要替代方案。

一旦您将绑定分类为具有合理替代方案的已发布绑定,没有合理替代方案的发布绑定和内部绑定,考虑将它们安排到这样的模块中:

  • 每个需要合理替代方案的已发布绑定作为一个模块。(如果你也在编写备选方案,则每个方案都有自己的模块。)该模块只包含一个已发布的绑定,以及发布绑定所需的所有内部绑定。
  • 所有不需要合理替代方案的已发布绑定都会进入沿功能线组织的模块。
  • 已发布的绑定模块应各自包含需要公共绑定的无需替代方案模块。

通过描述该模块提供了哪些已发布绑定来记录每个模块是个好主意。

下面是使用认证域的示例。如果存在 AuthManager 接口,则可能具有 OAuth 实现和用于测试的虚假实现。如上所述,你不希望在配置之间进行更改当前用户可能存在的明显的绑定。

/**
 * 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.
}

然后您的生产配置将使用真实模块,测试配置假模块,如上所述。

你不必使用 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));
  }
}

参考

Testing with Dagger