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

Mic*_*sky 4 unit-testing dependency-injection mocking guice mockito

我一直在学习依赖注入(例如Guice),在我看来,其中一个主要的驱动因素,可测试性,已经被Mocking(例如Mockito)很好地覆盖了. 依赖注入和模拟框架之间的区别(Ninject vs RhinoMock或Moq)是依赖注入和Mockito之间共性的一个很好的总结,但它没有提供在能力重叠时使用哪些指南.

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

A]仅使用Mockito

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

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

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

Jef*_*ica 19

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;
  }
}
Run Code Online (Sandbox Code Playgroud)

注意这个班级做了多少额外的工作:你实际上并没有使用你的轮胎或车轮而不是创造一个工作引擎,并且没有办法替换你的轮胎或车轮:任何生产或测试中的汽车必须具有真实性轮胎,真正的轮子,真正的引擎,以及真正记录到服务器的真实记录器.你先写哪一部分?

让我们这个类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;
  }
}
Run Code Online (Sandbox Code Playgroud)

现在,CarController不必关心轮胎,车轮,引擎或日志输出,您可以通过将它们传递给构造函数来替换您想要的任何引擎和记录器.通过这种方式,DI在生产中非常有用:通过更改单个模块,您可以切换Logger以记录到循环缓冲区或本地文件,或切换到增压引擎,或单独升级到SnowTires或RacingTires. 这也使得类更易于测试,因为现在替换实现变得更加容易:您可以编写自己的测试双精度程序,如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;
  }
}
Run Code Online (Sandbox Code Playgroud)

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

@Mock Engine mockEngine;
// To verify:
verify(mockEngine).start();
// Or stub:
doThrow(new RuntimeException()).when(mockEngine).start();
Run Code Online (Sandbox Code Playgroud)

......这就是他们一起工作的原因.依赖注入使您有机会编写组件(CarController),而不必关注自己的依赖关系(轮胎,轮子,ServerLogOutput),并根据您的意愿更改依赖项实现.然后Mockito允许您使用最少的样板创建这些替换实现,可以在任何地方注入,无论您喜欢什么.

附注:正如您在问题中提到的,Guice和Mockito都不应该成为您API的一部分.Guice可以是您的实现细节的一部分,也可能是构造函数策略的一部分; Mockito是您测试的一部分,不应对您的公共界面产生任何影响.然而,在开始实施之前,为OO设计和测试选择框架是一个很好的讨论.


更新,纳入评论:

  • 通常,您不会在单元测试中实际使用Guice; 您将使用各种对象手动调用@Inject构造函数,并测试您喜欢的测试双精度.请记住,测试状态而不是交互更简单,更清晰,因此您永远不会想要模拟数据对象,您几乎总是想要模拟远程或异步服务,并且使用轻量级伪造品可以更好地表示昂贵且有状态的对象.不要过度使用Mockito作为唯一的解决方案.

  • Mockito有自己的"依赖注入"功能@InjectMocks,@Mock即使没有setter ,它也会用相同名称/类型的字段替换被测系统的字段.这是用mocks替换依赖项的一个很好的技巧,但正如你所记录和链接的那样,如果添加了依赖项,它将无声地失败.考虑到这种缺点,并且考虑到它错过了DI提供的大部分设计灵活性,我从未需要使用它.