我一直在学习依赖注入(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 设计和测试框架的选择是在开始实现之前进行的一次很好的讨论。
更新,合并评论:
@InjectMocks
,它将用 @Mock
替换被测系统的字段即使没有 setter ,也具有相同名称/类型的字段。这是用模拟替换依赖项的好技巧,但正如您所指出和链接的那样,it will fail silently如果添加了依赖项。考虑到这个缺点,并且考虑到它错过了 DI 提供的大部分设计灵 active ,我从来没有需要使用它。 关于unit-testing - 如果我使用 Mockito,我什至需要 Guice 吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/27260860/