如果我使用Mockito,我甚至需要Guice吗?

时间:2014-12-02 23:18:34

标签: unit-testing dependency-injection mocking mockito guice

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

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

A]仅使用Mockito

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

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

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

2 个答案:

答案 0 :(得分:18)

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在生产中非常有用:通过更改单个模块,您可以切换Logger以记录到循环缓冲区或本地文件,或切换到增压引擎,或单独升级到SnowTires或RacingTires。  这也使得类更易于测试,因为现在替换实现变得更加容易:您可以编写自己的test doubles,例如FakeEngine和DummyLogger,并将它们放在CarControllerTest中。 (当然,您也可以创建setter方法或替代构造函数,并且可以在不实际使用Guice的情况下以这种方式设计类.Guice的强大功能来自于以松散耦合方式构造大型依赖图。)

现在,对于那些测试双打:在只有Guice但没有Mockito的世界中,你必须编写自己的Logger兼容测试双和你自己的Engine兼容测试双:

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;您将使用各种对象和您喜欢的test doubles手动调用@Inject构造函数。请记住,测试状态而不是交互更简单,更清晰,因此您永远不会想要模拟数据对象,您几乎总是想要模拟远程或异步服务,并且使用轻量级假货可以更好地表示昂贵且有状态的对象。不要过度使用Mockito作为唯一的解决方案。

  • Mockito有自己的“依赖注入”功能,称为@InjectMocks,它将使用相同名称/类型的@Mock字段替换被测系统的字段,即使没有setter方法。这是用mocks替换依赖项的一个很好的技巧,但正如你所注意到并链接的那样,it will fail silently如果添加了依赖项。考虑到这种缺点,并且考虑到它错过了DI提供的大部分设计灵活性,我从来没有必要使用它。

答案 1 :(得分:3)

看看Jukito,它是Mockito和Guice以及Junit的混合物。

https://github.com/ArcBees/Jukito

http://jukito.arcbees.com/