前段时间,我写了一篇有关使用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;
}
@Override
public void process(Report report) {
sender.send(report);
}
}
现在,我们的测试是什么样的? 我们需要验证什么? 我们必须检查报告是否传递给Sender实例的send()方法。 可以按照以下步骤完成:
public class DummyTest {
@Test
public 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<>();
@Override
public void add(Report report) {
reports.put(report.title(), report);
}
@Override
public boolean existsWithTitle(Title title) {
return reports.containsKey(title);
}
@Override
public int countAll() {
return reports.size();
}
@Override
public Report findByTitle(Title title) {
return reports.get(title);
}
}
我们的测试将如下所示:
public class FakeTest {
@Test
public 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 {
@Test
public 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。 我们正在检查它是否被调用。 我们可以检查它被调用了多少次。
我们也可以将实际的应用程序对象用作间谍。 无需创建任何其他类。
让我们从第一段回到ReportProcessor:
public class ReportProcessor implements Processor {
// code
@Override
public void process(Report report) {
sender.send(report);
}
}
可能您已经注意到我们在那里使用了Spy,但让我们再次看一下测试:
public class SpyTest {
@Test
public 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 {
//code
public 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;
@Test
public void shouldCreateReportIfDoesNotExist() {
given(reportRepository.existsWithTitle(DUMMY_TITLE)).willReturn(false);
reportService.create(DUMMY_TITLE, DUMMY_CONTENT);
then(reportRepository).should().add(anyReport());
}
@Test
public 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模式:Test Double 。
祝您测试顺利! 使它们可读且有价值。
如果您对“测试双模式”有任何想法或疑问,请在评论中分享。
翻译自: https://www.javacodegeeks.com/2015/09/test-double-patterns.html