双重for
前段时间,我写了一篇有关使用Test Double的后果的文章,但是与Test Double Patterns无关,仅是一个简单的清单。 今天,我想对其进行更改,并解释这些模式之间的差异。
正如我在提到的文章中写道:
Test Double是允许我们控制被测单元之间依赖性的模式。 为了能够在我们想要或/和/或验证是否发生想要的行为时提供想要的行为。
因此,现在当您想起了基础知识时,我们可以转到有趣的部分–让我们看一下“测试双重模式”。
虚拟对象
虚拟是TD(测试双精度),当我们想要传递对象以填充参数列表时使用。 从未实际使用过。 这就是为什么它不总是被视为TD之一的原因-它不提供任何行为。
假设我们有发送报告的Sender类。 由于某些要求,我们需要将其包装到另一个类中以提供有效的接口。 我们的课看起来像这样:
public class ReportProcessor implements Processor {private Sender sender;public ReportProcessor(Sender sender) {this.sender = sender;}@Overridepublic void process(Report report) {sender.send(report);}
}
现在,我们的测试是什么样的? 我们需要验证什么? 我们必须检查报告是否传递给Sender实例的send()方法。 可以按照以下步骤完成:
public class DummyTest {@Testpublic void shouldSentReportWhileProcessing() {Sender sender = aMessageSender();ReportProcessor reportProcessor = aReportProcessor(sender);Report dummyReport = new Report();reportProcessor.process(dummyReport);then(sender).should().send(dummyReport);}private ReportProcessor aReportProcessor(Sender sender) {return new ReportProcessor(sender);}private Sender aMessageSender() {return spy(Sender.class);}
}
如您所见,没有与我们的虚拟对象进行交互。 仅创建报告并将其作为参数传递。 没有行为,只有存在。
假物件
Fake Object只是测试类所依赖的对象的一种更简单,更轻量的实现。 它提供了预期的功能。
在决定时要记住的重要事项是使其尽可能简单。 任何其他逻辑可能会对测试的脆弱性和准确性产生重大影响。
假设我们有一个带有create()方法的ReportService,它的职责是仅在尚未创建Report的情况下创建一个Report。 为简单起见,我们可以假定标题标识一个对象–我们不能有两个标题相同的报表:
public class ReportService {private ReportRepository reportRepository;public ReportService(ReportRepository reportRepository) {this.reportRepository = reportRepository;}public void create(Title title, Content content) {if (!reportRepository.existsWithTitle(title)) {Report report = new Report(title, content);reportRepository.add(report);}}
}
我们可以通过多种方式测试此代码,但是我们将决定使用Fake Object:
class FakeReportRepository implements ReportRepository {private Map<Title, Report> reports = new HashMap<>();@Overridepublic void add(Report report) {reports.put(report.title(), report);}@Overridepublic boolean existsWithTitle(Title title) {return reports.containsKey(title);}@Overridepublic int countAll() {return reports.size();}@Overridepublic Report findByTitle(Title title) {return reports.get(title);}
}
我们的测试将如下所示:
public class FakeTest {@Testpublic void shouldNotCreateTheSameReportTwice() {FakeReportRepository reportRepository = new FakeReportRepository();ReportService reportService = aReportService(reportRepository);reportService.create(DUMMY_TITLE, DUMMY_CONTENT);reportService.create(DUMMY_TITLE, DUMMY_CONTENT);Report createdReport = reportRepository.findByTitle(DUMMY_TITLE);assertThat(createdReport.title()).isSameAs(DUMMY_TITLE);assertThat(createdReport.content()).isSameAs(DUMMY_CONTENT);assertThat(reportRepository.countAll()).isEqualTo(1);}private ReportService aReportService(ReportRepository reportRepository) {return new ReportService(reportRepository);}
}
存根对象
我们在对方法输出感兴趣的情况下使用Stub Object,以确保每次调用它时结果都将完全符合我们的期望。
通常,我们不会在测试中检查是否调用了Stub,因为我们会通过其他断言知道它。
在此示例中,我们将查看一个ReportFactory,该工厂将创建具有创建日期的报表。 为了可测试性,我们使用了依赖注入来注入DateProvider:
public class ReportFactory {private DateProvider dateProvider;public ReportFactory(DateProvider dateProvider) {this.dateProvider = dateProvider;}public Report crete(Title title, Content content) {return new Report(title, content, dateProvider.date());}
}
它允许我们在测试中使用Stub:
public class StubTest {@Testpublic void shouldCreateReportWithCreationDate() {Date dummyTodayDate = new Date();DateProvider dateProvider = mock(DateProvider.class);stub(dateProvider.date()).toReturn(dummyTodayDate);ReportFactory reportFactory = new ReportFactory(dateProvider);Report report = reportFactory.crete(DUMMY_TITLE, DUMMY_CONTENT);assertThat(report.creationDate()).isSameAs(dummyTodayDate);}
}
如您所见,我们仅对调用Stub的结果感兴趣。
间谍对象
与Stub对象相反,当我们对间谍方法的输入感兴趣时,我们将使用Spies。 我们正在检查它是否被调用。 我们可以检查它被调用了多少次。
我们也可以将实际的应用程序对象用作Spies。 无需创建任何其他类。
让我们从第一段回到ReportProcessor:
public class ReportProcessor implements Processor {// code@Overridepublic void process(Report report) {sender.send(report);}
}
可能您已经注意到我们在那里使用了Spy,但让我们再次看一下测试:
public class SpyTest {@Testpublic void shouldSentReportWhileProcessing() {Sender sender = spy(Sender.class);ReportProcessor reportProcessor = aReportProcessor(sender);reportProcessor.process(DUMMY_REPORT);then(sender).should().send(DUMMY_REPORT);}private ReportProcessor aReportProcessor(Sender sender) {return new ReportProcessor(sender);}
}
我们要检查对象是否以正确的方式包装,并将参数传递给其(包装的对象)方法调用。 这就是为什么我们在这里使用Spy。
模拟对象
模拟对象通常被描述为Stub和Spy的组合。 我们指定期望接收的输入,并在此基础上返回正确的结果。
如果这是我们期望的,则调用模拟对象也可能导致抛出异常。
好的,让我们再次看一下ReportService:
public class ReportService {//codepublic void create(Title title, Content content) {if (!reportRepository.existsWithTitle(title)) {Report report = new Report(title, content);reportRepository.add(report);}}
}
现在,我们将使用模拟对象代替伪对象:
@RunWith(MockitoJUnitRunner.class)
public class MockTest {@Mock private ReportRepository reportRepository;@InjectMocks private ReportService reportService;@Testpublic void shouldCreateReportIfDoesNotExist() {given(reportRepository.existsWithTitle(DUMMY_TITLE)).willReturn(false);reportService.create(DUMMY_TITLE, DUMMY_CONTENT);then(reportRepository).should().add(anyReport());}@Testpublic void shouldNotCreateReportIfDoesNotExist() {given(reportRepository.existsWithTitle(DUMMY_TITLE)).willReturn(true);reportService.create(DUMMY_TITLE, DUMMY_CONTENT);then(reportRepository).should(never()).add(anyReport());}private Report anyReport() {return any(Report.class);}
}
为了澄清一切,我们的模拟对象是ReportRepository.existsWithTitle()方法。 如您所见,在第一个测试中,我们说如果调用带有DUMMY_OBJECT参数的方法,它将返回true。 在第二个测试中,我们检查相反的情况。
我们在两个测试中的最后一个断言(then()。should())是另一个TD模式。 你能认出哪一个吗?
最后一句话
这就是我今天要说的有关测试双模式的全部内容。 我鼓励您有意使用它们,不要盲目遵循在可能的情况下添加@Mock注释的习惯。
我还邀请您阅读有关使用Test Double的后果的文章,以了解使用TD模式时可能遇到的问题以及如何识别和解决此类问题。
如果您想进一步加深对这些模式的了解,那么会有一个很棒的页面可以帮助您做到这一点: xUnit Patterns:Test Double 。
祝您测试顺利! 使它们可读且有价值。
如果您对“测试双模式”有任何想法或疑问,请在评论中分享。
翻译自: https://www.javacodegeeks.com/2015/09/test-double-patterns.html
双重for