Статьи

Тест двойных паттернов

Некоторое время назад я написал статью о последствиях использования Test Double , но в Test Double Patterns не было ничего, кроме простого списка. Сегодня я хотел бы изменить это и объяснить различия между этими образцами.

Как я уже писал в упомянутой статье, что:

Test Double — это шаблоны, которые позволяют нам контролировать зависимости между тестируемым модулем. Чтобы можно было обеспечить требуемое поведение всякий раз, когда мы хотим или / и проверить, произошло ли требуемое поведение.
Так что теперь, когда вам напомнили об основах, мы можем перейти к интересной части — давайте посмотрим на тестирование двойных шаблонов.

Пустышка

Dummy — это TD (Test Double), который используется, когда мы хотим передать объект для заполнения списка параметров. Это никогда не используется на самом деле. Вот почему он не всегда рассматривается как один из TD — он не обеспечивает никакого поведения.

Давайте предположим, что у нас есть класс Sender, который отправляет отчеты. Из-за некоторых требований нам нужно обернуть его в другой класс для обеспечения корректного интерфейса. Наш класс может выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
public class ReportProcessor implements Processor {
    private Sender sender;
 
    public ReportProcessor(Sender sender) {
        this.sender = sender;
    }
 
    @Override
    public void process(Report report) {
        sender.send(report);
    }
}

Теперь, как могут выглядеть наши тесты? Что нам нужно проверить? Мы должны проверить, был ли отчет передан методу send () экземпляра Sender. Это можно сделать следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
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 — это просто более простая и легкая реализация объекта, от которого зависит проверяемый класс. Это обеспечивает ожидаемую функциональность.

Важно помнить, когда вы принимаете решение, чтобы сделать это как можно более простым. Любая дополнительная логика может оказать большое влияние на хрупкость и точность ваших тестов.

Давайте предположим, что у нас есть ReportService с методом create (), и ответственность за него заключается в создании отчета только в том случае, если он еще не был создан. Для простоты мы можем предположить, что заголовок идентифицирует объект — у нас не может быть двух отчетов с одинаковым заголовком:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
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:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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);
    }
}

И наш тест будет выглядеть так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
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 или нет, потому что мы узнаем его из других утверждений.

В этом примере мы рассмотрим ReportFactory, который создает отчет с датой создания. Для удобства тестирования мы использовали Dependency Injection для внедрения DateProvider:

01
02
03
04
05
06
07
08
09
10
11
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 в нашем тесте:

01
02
03
04
05
06
07
08
09
10
11
12
13
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.

Шпионский объект

В противоположность объектам-заглушкам мы используем шпионы, когда мы заинтересованы во вводе шпионского метода. Мы проверяем, был ли он вызван или нет. Мы можем проверить, сколько раз это называлось.

Мы можем использовать реальные объекты приложений в качестве шпионов. Нет необходимости создавать какой-либо дополнительный класс.

Вернемся к нашему ReportProcessor из первого абзаца:

1
2
3
4
5
6
7
8
public class ReportProcessor implements Processor {
    // code
 
    @Override
    public void process(Report report) {
        sender.send(report);
    }
}

Возможно, вы уже заметили, что мы использовали там Spy, но давайте посмотрим на тест еще раз:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
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 здесь.

Макет объекта

Mock Object часто описывается как комбинация Stub и Spy. Мы уточняем, какой вклад мы ожидаем получить, и на основании этого мы получаем правильный результат.

Вызов Mock Object также может привести к исключению, если это то, что мы ожидали.

Хорошо, давайте еще раз посмотрим на наш ReportService:

01
02
03
04
05
06
07
08
09
10
public class ReportService {
    //code
 
    public void create(Title title, Content content) {
        if (!reportRepository.existsWithTitle(title)) {
            Report report = new Report(title, content);
            reportRepository.add(report);
        }
    }
}

Теперь вместо Fake Object мы будем использовать Mock Object:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@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);
    }
}

Чтобы прояснить все, наш Mock-объект является методом ReportRepository.existsWithTitle (). Как видите, в первом тесте мы говорим, что если мы вызовем метод с параметром DUMMY_OBJECT, он вернет true. Во втором тесте мы проверяем противоположный случай.

Наше последнее утверждение (then (). Should ()) в обоих тестах — это еще один шаблон TD. Вы можете узнать какой?

Последнее слово в конце

И это все, что я должен сказать о тестовых двойных моделях сегодня. Я призываю вас использовать их намеренно, а не слепо следовать привычке добавлять аннотации @Mock, когда это возможно.

Я также приглашаю вас прочитать статью о последствиях использования Test Double, чтобы узнать, с какими проблемами вы можете столкнуться при использовании TD Patterns и как распознать и решить такие проблемы.

Если вы хотите еще больше углубить свои знания об этих шаблонах, есть отличная страница, которая поможет вам в этом: xUnit Patterns: Test Double .

Удачи в ваших тестах! Сделайте их читабельными и ценными.

Если у вас есть какие-либо мысли или вопросы по поводу тестирования двойных шаблонов, просто поделитесь ими в комментариях.

Ссылка: Протестируйте Double Patterns от нашего партнера JCG Себастьяна Малаки в блоге « Давайте поговорим о Java» .