大多数班级都有合作者。 在进行单元测试时,您通常希望避免使用那些协作者的实际实现方式来避免测试的脆弱性和绑定/耦合,而应使用测试双打:模拟,存根和双打。 本文引用了有关该主题的两篇现有文章:Martin Fowler的Mocks Are n't Stubs和Bob Uncle的The Little Mocker 。 我都推荐他们。
术语
我将从Gerard Meszaros的书xUnit Test Patterns中借用一个术语。 在其中,他引入了术语“ 被测系统( SUT )”,即我们正在测试的东西。 “测试中的类”是更适用于面向对象的世界的一种替代方法,但是我会坚持使用SUT,因为Fowler也会这样做。
我还将使用状态验证和行为验证这两个术语。 状态验证是通过检查SUT或其协作者的状态来验证代码是否正常工作。 行为验证是在验证协作者是否按照我们期望的方式被调用或调用。
测试双打
好,回到如何与被测系统的合作者打交道。 对于SUT的每个协作者,您可以使用该协作者的实际实现。 例如,如果您有一个与数据访问对象(DAO)协作的服务,如下面的WidgetService示例中所示,则可以使用真实的DAO实现。 但是,它很可能与数据库冲突,这绝对不是我们要进行单元测试所需的数据库。 另外,如果DAO实现中的代码发生更改,则可能导致我们的测试开始失败。 我个人不喜欢当被测代码本身未更改时测试开始失败。
因此,我们可以使用有时称为“测试双打”的测试。 “测试双打”一词也来自Meszaros的xUnit测试模式书。 他将它们描述为“为了明确运行测试而安装的代替实际组件的任何对象或组件”。
在本文中,我将介绍我使用的三种主要的测试双打类型:模拟,存根和傻瓜。 我还将简要介绍两个我很少明确使用的东西:间谍和假货。
1.嘲弄
首先,“模拟”是一个过载的术语。 它通常用作任何测试双精度测试的总称; 也就是说,任何类型的对象都可以代替测试中的类中的真实协作者。 我对此感到满意,因为大多数模拟框架都支持此处讨论的大多数测试双打。 但是,出于本文的目的,我将以更严格,更有限的含义使用模拟。
具体来说, 模拟是一种使用行为验证的测试替身类型 。
马丁·福勒(Martin Fowler)将模拟描述为“用期望进行预编程的对象,这些对象构成了期望接收的调用的规范”。 正如Bob叔叔所说的那样,模拟程序会监视正在测试的模块的行为,并且知道期望的行为。 一个例子可以使它更清楚。
想象一下WidgetService的实现:
public class WidgetService {final WidgetDao dao;public WidgetService(WidgetDao dao) {this.dao = dao;}public void createWidget(Widget widget) {//misc business logic, for example, validating widget is valid//...dao.saveWidget(widget);}
}
我们的测试可能看起来像这样:
public class WidgetServiceTest {//test fixturesWidgetDao widgetDao = mock(WidgetDao.class);WidgetService widgetService = new WidgetService(widgetDao);Widget widget = new Widget();@Testpublic void createWidget_saves_widget() throws Exception {//call method under testwidgetService.createWidget(widget);//verify expectationverify(widgetDao).saveWidget(widget);}
}
我们创建了一个WidgetDao的模拟,并验证它是否如预期的那样被调用。 我们还可以告诉模拟程序在调用时如何响应。 这是模拟的重要组成部分,允许您操纵模拟,以便可以测试代码的特定单元,但是在这种情况下,测试不是必需的。
模拟框架
在此示例中,我将Mockito用于模拟框架,但Java空间中还有其他对象,包括EasyMock和JMock 。
自己动手玩?
请注意,您不必使用模拟框架即可使用模拟。 您也可以自己编写模拟,甚至可以在模拟中构建断言。 例如,在这种情况下,我们可以创建一个名为WidgetDaoMock的类,该类实现WidgetDao接口,并且该类的createWidget()方法的实现仅记录其被调用的情况。 然后,您可以验证呼叫是否按预期进行。 尽管如此,现代的模拟框架仍然使这种“劳碌自在”的解决方案变得多余。
2.存根
存根是为了测试目的而“存根”或提供实现的大大简化版本的对象。
例如,如果我们的WidgetService类现在也也依赖于ManagerService。 请参阅此处的标准化方法:
public class WidgetService {final WidgetDao dao;final ManagerService manager;public WidgetService(WidgetDao dao, ManagerService manager) {this.dao = dao;this.manager = manager;}public void standardize(Widget widget) {if (manager.isActive()) {widget.setStandardized(true);}}public void createWidget(Widget widget) {//omitted for brevity}
}
并且我们想测试当管理器处于活动状态时,标准化方法是否“标准化”了一个小部件,我们可以使用如下所示的存根:
public class WidgetServiceTest {WidgetDao widgetDao = mock(WidgetDao.class);Widget widget = new Widget();class ManagerServiceStub extends ManagerService {@Overridepublic boolean isActive() {return true;}}@Testpublic void standardize_standardizes_widget_when_active() {//setupManagerServiceStub managerServiceStub = new ManagerServiceStub();WidgetService widgetService = new WidgetService(widgetDao, managerServiceStub);//call method under testwidgetService.standardize(widget);//verify stateassertTrue(widget.isStandardized());}
}
由于模拟通常用于行为验证,而存根可用于状态验证或行为验证。
该示例非常基础,也可以使用模拟来完成,但是存根可以为测试夹具的可配置性提供一种有用的方法。 我们可以对ManagerServiceStub进行参数化,以使其将“活动”字段的值用作构造函数参数,因此可以在否定测试用例中重用。 也可以使用更复杂的参数和行为。 其他选项包括将存根创建为匿名内部类,或为存根创建基类,例如ManagerServiceStubBase,以供其他人扩展。 后者的优点是,如果ManagerService接口发生更改,则只有ManagerServiceStubBase类会中断,并且需要更新。
我倾向于经常使用存根。 我喜欢他们提供的灵活性,以便能够自定义测试装置,并提供纯Java代码提供的清晰度。 将来的维护者不需要能够理解某个框架。 我的大多数同事似乎更喜欢使用模拟框架。 找到最适合您的方法,并运用最佳判断。
3.假人
顾名思义,虚拟对象是非常愚蠢的类。 它几乎不包含任何内容,基本上只足以使您的代码得以编译。 当您不在乎如何使用虚拟对象时,可以将其传递给某些对象。 例如,作为测试的一部分,当您必须传递参数时,但是您不希望使用该参数。
例如,在前面的示例中的standardize_standardizes_widget_when_active()测试中,我们仍然继续使用模拟的WidgetDao。 虚拟对象可能是一个更好的选择,因为我们根本不希望在createWidget()方法中完全使用WidgetDao。
public class WidgetServiceTest {Widget widget = new Widget();class ManagerServiceStub extends ManagerService {@Overridepublic boolean isActive() {return true;}}class WidgetDaoDummy implements WidgetDao {@Overridepublic Widget getWidget() {throw new RuntimeException("Not expected to be called");}@Overridepublic void saveWidget(Widget widget) {throw new RuntimeException("Not expected to be called");}}@Testpublic void standardize_standardizes_widget_when_active() {//setupManagerServiceStub managerServiceStub = new ManagerServiceStub();WidgetDaoDummy widgetDao = new WidgetDaoDummy();WidgetService widgetService = new WidgetService(widgetDao, managerServiceStub);//call method under testwidgetService.standardize(widget);//verify stateassertTrue(widget.isStandardized());}
}
在这种情况下,我创建了一个内部类。 在大多数情况下,由于Dummy功能很少会在测试之间发生变化,因此创建非内部类并为所有测试重用更为有意义。
还要注意在这种情况下,使用模拟框架创建类的模拟实例也是可行的选择。 我个人很少使用假人,而是创建这样的模拟:
WidgetDaoDummy widgetDao = mock(WidgetDao.class);
尽管可以肯定的是,当确实发生意外调用时,抛出异常会更加困难(这取决于您选择的模拟框架),但是它确实具有简洁性的巨大优势。 虚拟变量可能很长,因为它们需要在接口中实现每种方法。
与存根一样,假人可用于状态或行为验证。
间谍与伪造
我将简要介绍另外两种测试双打:间谍和伪造。 我之所以简短地说,是因为我个人很少自己明确使用这两种类型的双打,而且还因为术语可能会引起混乱,而又不会引起更多细微差别! 但是为了完整性……
间谍
当您想确保系统调用了某个方法时,可以使用间谍程序。 它还可以记录各种事情,例如计算调用次数,或记录每次传递的参数。
但是,对于间谍来说,存在将测试与代码实现紧密耦合的危险。
间谍专用于行为验证。
大多数现代的模拟框架也很好地涵盖了这种功能。
假货
马丁·福勒(Martin Fowler)对伪造品的描述如下:伪造品具有有效的实现方式,但通常采取一些捷径,这使其不适合生产(内存数据库是一个很好的例子)。
我个人很少使用它们。
结论
测试双打是单元测试不可或缺的一部分。 嘲笑,存根和双打都是有用的工具,了解它们之间的差异很重要。
从最严格的意义上讲,模拟只是使用行为验证的双精度形式。 指定了两倍的期望值,然后在调用SUT时进行验证。 但是,工作模拟也已经变得越来越笼统地描述了此处描述的任何双打,实际上大多数现代模拟框架都可以这种通用方式使用。
最后,您应该使用哪种双精度型? 这取决于所测试的代码,但是我建议您遵循使您的测试意图最清楚的任何方式进行指导。
资料来源
- 莫蒂不是存根作者 ,马丁·福勒(Martin Fowler)
- 小嘲笑 ,“叔叔”鲍勃·马丁
- xUnit测试模式 ,作者Gerard Meszaros
翻译自: https://www.javacodegeeks.com/2015/11/test-doubles-mocks-dummies-and-stubs.html