如果我使用Mockito,我什至需要Guice



我一直在学习依赖注入(例如Guice(,在我看来,主要驱动因素之一,可测试性,已经被Moking(例如Mockito(很好地涵盖了。 依赖注入和模拟框架(Ninject vs RhinoMock或Moq(之间的区别很好地总结了依赖注入和Mockito之间的共性,但它没有提供当它们的功能重叠时使用的指导。

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

A] 仅使用模拟

B] 使用 Guice 并设计两个接口实现 - 一个用于真实接口,一个用于测试

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

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

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 不必关心轮胎、车轮、发动机或日志输出,您可以通过将它们传递到构造函数中来替换您想要的任何引擎和记录器。通过这种方式,DI 在生产中很有用:通过更改单个模块,您可以将记录器切换到记录到循环缓冲区或本地文件,或切换到增压引擎,或单独升级到雪地轮胎或赛车轮胎。 这也使该类更具可测试性,因为现在替换实现变得更加容易:您可以编写自己的测试替身,例如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();

。这就是为什么他们合作得如此之好。依赖注入让你有机会编写一个组件(CarController(,而不用关心它的依赖关系(轮胎、车轮、ServerLogOutput(,并随意更改依赖实现。然后,Mockito允许您使用最少的样板创建这些替换实现,可以在您想要的任何位置和方式注入。

旁注:正如您在问题中提到的,Guice 和 Mockito 都不应该成为您的 API 的一部分。Guice 可以是实现细节的一部分,也可以是构造函数策略的一部分;Mockito是测试的一部分,不应该对你的公共接口产生任何影响。尽管如此,在开始实现之前,选择用于OO设计和测试的框架是一个很好的讨论。


更新,并纳入评论:

  • 通常,您实际上不会在单元测试中使用 Guice;您将使用您喜欢的对象和测试替身的分类手动调用@Inject构造函数。请记住,测试状态比测试交互更容易、更干净,因此您永远不会想要模拟数据对象,您几乎总是希望模拟远程或异步服务,并且昂贵且有状态的对象可能更好地用轻量级伪造来表示。不要试图过度使用Mockito作为唯一的解决方案。

  • Mockito有自己的"依赖注入"功能,称为@InjectMocks,即使没有setter,它也会将被测系统的字段替换为相同名称/类型的@Mock字段。这是用模拟替换依赖项的好技巧,但正如您指出和链接的那样,如果添加依赖项,它将静默失败。鉴于这个缺点,并且考虑到它错过了 DI 提供的大部分设计灵活性,我从来没有需要使用它。

看看Jukito,它是Mockito和Guice和Junit的混合体。

https://github.com/ArcBees/Jukito

http://jukito.arcbees.com/

相关内容

  • 没有找到相关文章

最新更新