Статьи

Test Doubles: издевается, манекены и заглушки

У большинства классов есть сотрудники. При модульном тестировании вы обычно хотите избегать использования реальных реализаций этих соавторов, чтобы избежать хрупкости теста и связывания / связывания, и вместо этого использовать Test Doubles: Mocks, Stubs и Doubles. Эта статья ссылается на две существующие статьи на эту тему: « Насмешки не заглушки » Мартина Фаулера и «Маленький пересмешник » Дядюшки Боба Мартина. Я рекомендую их обоих.

терминология

Я собираюсь позаимствовать термин из книги Джерарда Месароса xUnit Test Patterns . В нем он вводит термин «тестируемая система», то есть то, что мы тестируем. Тестируемый класс — это альтернатива, которая более применима в объектно-ориентированном мире, но я буду придерживаться SUT, поскольку Фаулер также делает это.

Я также буду использовать условия проверки состояния и проверки поведения . Проверка состояния — это проверка работоспособности кода путем проверки состояния SUT или его соавторов. Проверка поведения — это проверка того, что сотрудники вызывались или вызывались так, как мы ожидали.

Test Doubles

Хорошо, вернемся к тому, как обращаться с сотрудниками тестируемой системы. Для каждого сотрудника SUT вы можете использовать реальную реализацию этого сотрудника. Например, если у вас есть служба, которая взаимодействует с объектом доступа к данным (DAO), как в приведенном ниже примере WidgetService, вы можете использовать реальную реализацию DAO. Однако это, скорее всего, идет вразрез с базой данных, что определенно не то, что мы хотим для модульного теста. Кроме того, если код в реализации DAO изменился, это может вызвать сбой нашего теста. Лично мне не нравятся тесты, начинающие проваливаться, когда сам тестируемый код не менялся.

Таким образом, вместо этого мы можем использовать то, что иногда называют Test Doubles. Термин Test Doubles также взят из книги Meszaros xUnit Test Patterns . Он описывает их как «любой объект или компонент, который мы устанавливаем вместо реального компонента для явной цели выполнения теста».

В этой статье я расскажу о трех основных типах двойных тестов, которые я использую: Mocks, Stubs и Dummies. Я также кратко расскажу о двух, которые я редко использую явно: шпионы и подделки.

1. издевается

Во-первых, «макет» — это перегруженный термин. Он часто используется как общий термин для любого двойного теста; то есть любой тип объекта, используемый в качестве замены для реального сотрудника в тестируемом классе. Мне это удобно, так как большинство фальшивых фреймворков поддерживают большинство тестовых двойников, обсуждаемых здесь. Но для целей этой статьи я буду использовать mock в более строгом, более ограниченном смысле.

В частности, макет — это тип тестового двойника, который использует проверку поведения .

Мартин Фаулер описывает макеты как «объекты, предварительно запрограммированные с ожиданиями, которые формируют спецификацию вызовов, которые они ожидают получить». Где, как дядя Боб говорит, что издевательство следит за поведением тестируемого модуля и знает, какое поведение ожидать. Пример может прояснить ситуацию.

Представьте себе эту реализацию WidgetService:

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

Наш тест может выглядеть примерно так:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
public class WidgetServiceTest {
 
    //test fixtures
    WidgetDao widgetDao = mock(WidgetDao.class);
    WidgetService widgetService = new WidgetService(widgetDao);
    Widget widget = new Widget();
 
    @Test
    public void createWidget_saves_widget() throws Exception {
        //call method under test
        widgetService.createWidget(widget);
 
        //verify expectation
        verify(widgetDao).saveWidget(widget);
    }
}

Мы создали макет WidgetDao и убедились, что он вызывался так, как мы и ожидали. Мы могли бы также сказать, как реагировать, когда он вызывался; это большая часть mock-ов, позволяющая вам манипулировать mock-ом, чтобы вы могли протестировать определенный блок вашего кода, но в этом случае он не нужен для теста.

Насмешливые рамки

В этом примере я использую Mockito для фреймворка Mocking , но в пространстве Java есть и другие, в том числе EasyMock и JMock .

Сверните свои собственные издевательства?

Обратите внимание, что вам не нужно использовать фреймворки для использования макетов. Вы тоже можете написать макеты и даже встроить утверждение в макет. В этом случае, например, мы могли бы создать класс с именем WidgetDaoMock, который реализует интерфейс WidgetDao, и реализация которого метода createWidget () просто записывает, что он был вызван. Затем вы можете убедиться, что звонок был сделан, как и ожидалось. Тем не менее, современные фальшивые фреймворки делают такое решение по принципу «сам по себе» в значительной степени излишним.

2. Заглушка

Заглушка — это объект, который «заглушает» или предоставляет значительно упрощенную версию реализации для целей тестирования.

Например, если наш класс WidgetService теперь также полагается на ManagerService. Смотрите метод стандартизации здесь:

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

И мы хотим проверить, что метод стандартизации «стандартизирует» виджет, когда менеджер активен, мы могли бы использовать заглушку следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class WidgetServiceTest {
 
    WidgetDao widgetDao = mock(WidgetDao.class);
    Widget widget = new Widget();
 
    class ManagerServiceStub extends ManagerService {
        @Override
        public boolean isActive() {
            return true;
        }
    }
    @Test
    public void standardize_standardizes_widget_when_active() {
        //setup
        ManagerServiceStub managerServiceStub = new ManagerServiceStub();
        WidgetService widgetService = new WidgetService(widgetDao, managerServiceStub);
 
        //call method under test
        widgetService.standardize(widget);
 
        //verify state
        assertTrue(widget.isStandardized());
    }
}

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

Этот пример очень простой, и его можно было бы сделать и с помощью макета, но заглушки могут обеспечить полезный подход для конфигурирования тестовых устройств. Мы могли бы параметризовать ManagerServiceStub, чтобы он принимал значение для «активного» поля в качестве аргумента конструктора и, следовательно, мог быть повторно использован для отрицательного теста. Также могут использоваться более сложные параметры и поведение. Другими вариантами являются создание заглушки в качестве анонимного внутреннего класса или создание базового класса для заглушки, такого как ManagerServiceStubBase, для расширения других пользователей. Преимущество последнего состоит в том, что в случае изменения интерфейса ManagerService будет нарушен только класс ManagerServiceStubBase, который необходимо обновить.

Я часто использую окурки. Мне нравится гибкость, которую они предоставляют, чтобы иметь возможность настраивать тестовое устройство, и ясность, которую они обеспечивают от простого старого Java-кода. Не нужно, чтобы будущие сопровождающие могли понимать определенные рамки. Большинство моих коллег предпочитают использовать фальшивые рамки. Найдите то, что работает для вас, и используйте свое лучшее суждение.

3. Пустышка

Как следует из названия, манекен очень тупой класс. Он почти ничего не содержит, в основном достаточно для компиляции кода. Вы превращаете пустышку во что-то, когда вам все равно, как оно используется. например, как часть теста, когда вы должны передать аргумент, но вы не ожидаете, что аргумент будет использоваться.

Например, в тесте standardize_standardizes_widget_when_active () в предыдущем примере мы все еще продолжали использовать фиктивный WidgetDao. Манекен может быть лучшим выбором, так как мы никогда не ожидаем, что WidgetDao вообще будет использоваться в методе createWidget ().

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
28
29
30
31
32
33
34
public class WidgetServiceTest {
 
    Widget widget = new Widget();
 
    class ManagerServiceStub extends ManagerService {
        @Override
        public boolean isActive() {
            return true;
        }
    }
    class WidgetDaoDummy implements WidgetDao {
        @Override
        public Widget getWidget() {
            throw new RuntimeException("Not expected to be called");
        }
        @Override
        public void saveWidget(Widget widget) {
            throw new RuntimeException("Not expected to be called");
        }
    }
    @Test
    public void standardize_standardizes_widget_when_active() {
        //setup
        ManagerServiceStub managerServiceStub = new ManagerServiceStub();
        WidgetDaoDummy widgetDao = new WidgetDaoDummy();
        WidgetService widgetService = new WidgetService(widgetDao, managerServiceStub);
 
        //call method under test
        widgetService.standardize(widget);
 
        //verify state
        assertTrue(widget.isStandardized());
    }
}

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

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

1
WidgetDaoDummy widgetDao = mock(WidgetDao.class);

Хотя, по общему признанию, может быть сложнее генерировать исключения, когда происходят неожиданные вызовы (это зависит от вашего фальшивого фреймворка по выбору), оно имеет большое преимущество краткости. Макеты могут быть длинными, так как им нужно реализовать каждый метод в интерфейсе.

Как и в случае с заглушками, манекены можно использовать для проверки состояния или поведения.

Шпионы и Подделки

Я кратко расскажу о двух других типах тестовых двойников: шпионах и подделках. Я кратко говорю, потому что лично я редко использую эти два типа парных выражений в явном виде, но также потому, что терминология может быть достаточно запутанной, не вводя больше нюансов! Но в интересах полноты …

шпион

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

Однако со шпионами существует опасность того, что ваши тесты будут тесно связаны с реализацией вашего кода.

Шпионы используются исключительно для проверки поведения.

Этот тип функциональности также очень хорошо покрыт большинством современных фреймворков.

Подделки

Мартин Фаулер описывает подделки следующим образом: подделки имеют рабочие реализации, но обычно используют некоторые ярлыки, которые делают их непригодными для производства (хороший пример — база данных в памяти).

Я лично редко, если когда-либо использую их.

Вывод

Test Doubles являются неотъемлемой частью модульного тестирования. Mocks, Stubs и Doubles являются полезными инструментами, и важно понимать разницу.

Насмешка в самом строгом смысле этого слова — просто двойник, использующий проверку поведения; то есть ожидания по двойному указываются, а затем проверяются при вызове SUT. Тем не менее, рабочий макет также вырос, чтобы более широко описать любой из описанных здесь двойников, и, действительно, большинство современных фальшивых фреймворков можно использовать таким же образом.

Наконец, какой тип двойного вы должны использовать? Это зависит от тестируемого кода, но я рекомендую руководствоваться тем, что проясняет цель ваших тестов.

источники