Статьи

Эффективное корпоративное тестирование — модульные тесты (2/6)

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

Модульные тесты

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

По моему опыту, большинство корпоративных разработчиков достаточно хорошо понимают, как создаются модульные тесты. Вы можете взглянуть на этот пример в моем проекте по тестированию кофе, чтобы получить представление. Большинство проектов используют JUnit в сочетании с Mockito для моделирования зависимостей и в идеале AssertJ для эффективного определения читаемых утверждений. Я всегда обращаю внимание на то, что мы можем выполнять модульные тесты без специальных расширений или бегунов, то есть запускать их только с простым JUnit. Причина этого проста: время выполнения; мы должны быть в состоянии выполнить сотни тестов в течение нескольких миллисекунд.

Модульные тесты обычно выполняются очень быстро, и они легко поддерживают создание сложных наборов тестов или специальных рабочих процессов разработки, поскольку они просты в выполнении и не налагают ограничений на жизненный цикл набора тестов.

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

Исходя из опыта работы с проектами, модульные тесты очень эффективны при тестировании кода, который имеет высокую плотность лаконичной логики или функциональности, например, реализацию определенного алгоритма, и в то же время не слишком сильно взаимодействует с другими компонентами. Чем менее плотный или сложный код в конкретном классе, чем ниже цикломатическая сложность или выше взаимодействие с другими компонентами, тем менее эффективны юнит-тесты при тестировании этого класса. Особенно в микросервисах со сравнительно небольшим количеством специализированной бизнес-логики и высокой степенью интеграции с внешними системами, возможно, меньше необходимости в проведении множества модульных тестов. Отдельные блоки этих систем обычно содержат мало специализированной логики, за исключением нескольких исключений. Это необходимо учитывать при выборе компромисса, на который мы тратим свое время и силы.

Тесты использования

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

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

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

Чтобы получить лучшее представление, представьте, что мы тестируем вариант использования заказа кофе, который включает в себя два класса CoffeeShop и OrderProcessor .

Тестовые двойные классы CoffeeShopTestDouble и OrderProcessorTestDouble или *TD находятся в тестовой области проекта, в то время как они расширяют компоненты CoffeeShop и OrderProcessor которые находятся в основной области. Двойники теста могут установить необходимую логику насмешек и разводки и потенциально расширить общедоступный интерфейс класса с помощью методов насмешек или проверки, связанных со случаем.

Ниже показан тестовый двойной класс для компонента CoffeeShop :

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public class CoffeeShopTestDouble extends CoffeeShop {
 
    public CoffeeShopTestDouble(OrderProcessorTestDouble orderProcessorTestDouble) {
        entityManager = mock(EntityManager.class);
        orderProcessor = orderProcessorTestDouble;
    }
 
    public void verifyCreateOrder(Order order) {
        verify(entityManager).merge(order);
    }
 
    public void verifyProcessUnfinishedOrders() {
        verify(entityManager).createNamedQuery(Order.FIND_UNFINISHED, Order.class);
    }
 
    public void answerForUnfinishedOrders(List<Order> orders) {
        // setup entity manager mock behavior
    }
}

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

Двойные классы тестирования являются повторно используемыми компонентами, которые пишутся один раз для каждой области проекта и используются в тестах с несколькими вариантами использования :

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
class CoffeeShopTest {
 
    private CoffeeShopTestDouble coffeeShop;
    private OrderProcessorTestDouble orderProcessor;
 
    @BeforeEach
    void setUp() {
        orderProcessor = new OrderProcessorTestDouble();
        coffeeShop = new CoffeeShopTestDouble(orderProcessor);
    }
 
    @Test
    void testCreateOrder() {
        Order order = new Order();
        coffeeShop.createOrder(order);
        coffeeShop.verifyCreateOrder(order);
    }
 
    @Test
    void testProcessUnfinishedOrders() {
        List<Order> orders = Arrays.asList(...);
        coffeeShop.answerForUnfinishedOrders(orders);
 
        coffeeShop.processUnfinishedOrders();
 
        coffeeShop.verifyProcessUnfinishedOrders();
        orderProcessor.verifyProcessOrders(orders);
    }
 
}

Тест варианта использования проверяет обработку отдельного варианта использования, который вызывается в точке входа, здесь CoffeeShop . Эти тесты становятся краткими и очень удобочитаемыми, поскольку в отдельных тестах происходит разводка и verifyProcessOrders() , и они могут, кроме того, использовать методы проверки для конкретного случая использования, такие как verifyProcessOrders() .

Как видите, test double расширяет класс рабочей области для настройки макетов и для методов проверки поведения. Хотя это выглядит как некоторая попытка настройки, затраты быстро амортизируются, если у нас есть несколько вариантов использования, которые могут повторно использовать компоненты в рамках всего проекта. Чем больше будет расти наш проект, тем больше будут преимущества этого подхода, особенно если мы посмотрим на время выполнения теста. Все наши тесты по-прежнему выполняются с использованием JUnit, который выполняет сотни из них в кратчайшие сроки.

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

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

Опубликовано на Java Code Geeks с разрешения Себастьяна Дашнера, партнера нашей программы JCG. Смотрите оригинальную статью здесь: Эффективное корпоративное тестирование — юнит-тесты (2/6)

Мнения, высказанные участниками Java Code Geeks, являются их собственными.