unit-testing - 如果我使用 Mockito,我什至需要 Guice 吗?

标签 unit-testing dependency-injection mocking mockito guice

我一直在学习依赖注入(inject)(例如 Guice),在我看来,主要驱动因素之一,可测试性,已经被 Mocking(例如 Mockito)很好地涵盖了。 Difference between Dependency Injection and Mocking framework (Ninject vs RhinoMock or Moq)是 Dependency Injection 和 Mockito 之间共性的一个很好的总结,但它没有提供当它们在能力重叠时使用的指导。

我即将设计一个 API,我想知道我是否应该:

A] 仅使用 Mockito

B] 使用 Guice 并设计两种接口(interface)实现——一种用于真实,一种用于测试

C] 一起使用 Mockito 和 Guice——如果是这样,怎么做?

我猜正确的答案是 C,同时使用它们,但我想要一些智慧的话:我在哪里可以使用依赖注入(inject)或模拟,我应该选择哪个,为什么?

最佳答案

Guice 和 Mockito 有非常不同和互补的角色,我认为他们一起工作最好。

考虑这个人为的示例类:

public class CarController {
  private final Tires tires = new Tires();
  private final Wheels wheels = new Wheels(tires);
  private final Engine engine = new Engine(wheels);
  private Logger engineLogger;

  public Logger start() {
    engineLogger = new EngineLogger(engine, new ServerLogOutput());
    engine.start();
    engineLogger.recordEvent(ENGINE_STARTED);
    return engineLogger;
  }
}

注意这门课做了多少额外的工作:你实际上并没有使用你的轮胎或轮子,除了创造一个工作的引擎,而且没有办法替代你的轮胎或轮子:任何汽车,在生产或测试中,必须有真正的轮胎、真实的车轮、真实的引擎和真实记录到服务器的真实记录器。你先写哪一部分?

让我们让这个类对 DI 友好:

public class CarController { /* with injection */
  private final Engine engine;
  private final Provider<Logger> loggerProvider;
  private Logger engineLogger;

  /** With Guice, you can often keep the constructor package-private. */
  @Inject public Car(Engine engine, Provider<Logger> loggerProvider) {
    this.engine = engine;
    this.loggerProvider = loggerProvider
  }

  public Logger start() {
    engineLogger = loggerProvider.get();
    engine.start();
    engineLogger.recordEvent(ENGINE_STARTED);
    return engineLogger;
  }
}

现在 CarController 不必关心轮胎、车轮、引擎或日志输出,您可以通过将它们传递给构造函数来替换您想要的任何 Engine 和 Logger。这样,DI在生产中就很有用了:通过改变单个模块,您可以将Logger切换到循环缓冲区或本地文件,或者切换到增压引擎,或者单独升级到SnowTires或RacingTires。
这也使该类更易于测试,因为现在替换实现变得更加容易:您可以编写自己的 test doubles例如 FakeEngine 和 DummyLogger 并将它们放在您的 CarControllerTest 中。 (当然,您也可以创建 setter 方法或替代构造函数,并且您可以在不实际使用 Guice 的情况下以这种方式设计类。Guice 的强大之处在于以松散耦合的方式构建大型依赖图。)

现在,对于那些测试替身:在一个只有 Guice 而没有 Mockito 的世界里,你必须编写自己的与 Logger 兼容的测试替身和自己的与引擎兼容的测试替身:

public class FakeEngine implements Engine {
  RuntimeException exceptionToThrow = null;
  int callsToStart = 0;
  Logger returnLogger = null;

  @Override public Logger start() {
    if (exceptionToThrow != null) throw exceptionToThrow;
    callsToStart += 1;
    return returnLogger;
  }
}

使用 Mockito,这变得自动化,具有更好的堆栈跟踪和更多功能:

@Mock Engine mockEngine;
// To verify:
verify(mockEngine).start();
// Or stub:
doThrow(new RuntimeException()).when(mockEngine).start();

...这就是为什么他们一起工作得这么好。依赖注入(inject)使您有机会编写组件(CarController),而无需考虑其依赖项的依赖项(Tires、Wheels、ServerLogOutput),并可以随意更改依赖项实现。然后,Mockito 允许您使用最少的样板创建这些替换实现,可以将其注入(inject)到您想要的任何位置。

旁注:正如您在问题中提到的那样,Guice 和 Mockito 都不应该成为您的 API 的一部分。 Guice 可以是你的实现细节的一部分,也可以是你的构造器策略的一部分; Mockito 是您测试的一部分,不应对您的公共(public)界面产生任何影响。然而,OO 设计和测试框架的选择是在开始实现之前进行的一次很好的讨论。

更新,合并评论:
  • 通常,您实际上不会在单元测试中使用 Guice。您将使用各种对象和 test doubles 手动调用 @Inject 构造函数。你比较喜欢。请记住,测试状态比测试交互更容易和更清晰,因此您永远不会想要模拟数据对象,您几乎总是想要模拟远程或异步服务,并且昂贵和有状态的对象可能更好地表示为轻量级假货.不要试图过度使用 Mockito 作为唯一的解决方案。
  • Mockito 有自己的“依赖注入(inject)”功能,称为 @InjectMocks ,它将用 @Mock 替换被测系统的字段即使没有 setter ,也具有相同名称/类型的字段。这是用模拟替换依赖项的好技巧,但正如您所指出和链接的那样,it will fail silently如果添加了依赖项。考虑到这个缺点,并且考虑到它错过了 DI 提供的大部分设计灵 active ,我从来没有需要使用它。
  • 关于unit-testing - 如果我使用 Mockito,我什至需要 Guice 吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27260860/

    相关文章:

    unit-testing - 如何在我的 wicket 单元测试中控制浏览器代理

    android - Kotlin 中的 Dagger 2 范围和依赖项

    testing - 如何在 Go 中使用 gomock 模拟函数?

    ios - OCMock [OCMAnyConstraint isProxy] : message sent to deallocated instance, 发生了什么事?

    java - DI 的部分模拟?

    c# - 注册通用工厂

    javascript - 在 xbl/xul 文件中加载 javascript 模块的正确方法是什么?

    JAVA:与接口(interface)的实现相比,InvocationHandler 有哪些优势?

    python - 如何模拟对象方法返回值

    unit-testing - pytest autouse fixture 导致测试失败