测试双重图案

前段时间,我写了一篇有关使用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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值