Статьи

Введение в Java TDD — часть 1

Добро пожаловать в серию тестовых разработок (TDD). Мы будем говорить о Java и JUnit в контексте TDD, но это всего лишь инструменты. Основная цель статьи — дать вам полное представление о TDD независимо от языка программирования и среды тестирования.

Если вы не используете TDD в своем проекте, вы либо ленивы, либо просто не знаете, как работает TDD. Оправдания о нехватке времени здесь не применяются.

Об этом посте

В этом посте я объясню, что такое TDD и как его можно использовать в Java. Какое место модульного тестирования занимает в TDD. То, что вы должны покрыть ваши юнит-тесты. И, наконец, какие принципы вы должны придерживаться, чтобы написать хорошие и эффективные модульные тесты.

Если вы уже знаете все о TDD в Java, но вас интересуют примеры и учебные пособия, я рекомендую вам пропустить эту часть и перейти к следующей (она будет опубликована через неделю после этой).

Что такое TDD?

Если кто-то попросит меня объяснить TDD в нескольких словах, я скажу, что TDD — это разработка тестов до реализации функциональности. Вы можете поспорить: сложно проверить вещи, которых еще нет. И, вероятно, Кент Бек даст тебе пощечину за это.

Так как это возможно? Это можно описать следующими шагами:

1. Вы читаете и понимаете требования для конкретной функции.
2. Вы разрабатываете набор тестов, которые проверяют особенность. Все тесты красные, из-за отсутствия реализации функции.
3. Вы разрабатываете функцию, пока все тесты не станут зелеными.
4. Рефакторинг кода.

BDD-поток

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

Разработка с использованием TDD имеет ценные преимущества:

1. Вы лучше понимаете, какую функцию вы реализуете.
2. У вас есть надежные показатели полноты функции.
3. Код покрыт тестами и имеет меньше шансов быть испорченным исправлениями или новыми функциями.

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

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

Место юнит-тестов в TDD

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

Место-на-блок-тест-в-тестирования-пирамиде

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

Давайте рассмотрим класс Account:

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public class Account {
 
    private String id = RandomStringUtils.randomAlphanumeric(6);
    private boolean status;
    private String zone;
    private BigDecimal amount;
 
    public Account() {
        status = true;
        zone = Zone.ZONE_1.name();
        amount = createBigDecimal(0.00);
    }
 
    public Account(boolean status, Zone zone, double amount) {
        this.status = status;
        this.zone = zone.name();
        this.amount = createBigDecimal(amount);
    }
 
    public enum Zone {
        ZONE_1, ZONE_2, ZONE_3
    }
 
    public static BigDecimal createBigDecimal(double total) {
        return new BigDecimal(total).setScale(2, BigDecimal.ROUND_HALF_UP);
    }
 
    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append("id: ").append(getId())
                .append("\nstatus: ")
                .append(getStatus())
                .append("\nzone: ")
                .append(getZone())
                .append("\namount: ")
                .append(getAmount());
        return sb.toString();
    }
 
    public String getId() {
        return id;
    }
 
    public boolean getStatus() {
        return status;
    }
 
    public void setStatus(boolean status) {
        this.status = status;
    }
 
    public String getZone() {
        return zone;
    }
 
    public void setZone(String zone) {
        this.zone = zone;
    }
 
    public BigDecimal getAmount() {
        return amount;
    }
 
    public void setAmount(BigDecimal amount) {
        if (amount.signum() < 0)
            throw new IllegalArgumentException("The amount does not accept negative values");
        this.amount = amount;
    }
}

В классе есть 4 метода получения. Обратите на них особое внимание. Если мы создадим отдельный модульный тест для каждого метода получения, мы получим слишком много избыточных строк кода. Эта ситуация может быть обработана с помощью тестирования поведения . Представьте, что нам нужно проверить правильность создания объекта, используя один из его конструкторов. Как проверить, что объект создан как положено? Нам нужно проверить значение каждого поля. Следовательно, геттеры могут быть использованы в этом сценарии.

Создайте небольшие и быстрые модульные тесты , потому что они должны выполняться каждый раз перед фиксацией в git-репозитории и новой сборкой на сервере. Вы можете рассмотреть пример с действительными числами, чтобы понять важность скорости юнит-тестов. Давайте предположим, что проект имеет 1000 модульных тестов. Каждый из них занимает 100 мс. В результате выполнение всех тестов занимает 1 минуту 40 секунд.

На самом деле 100 мс — это слишком долго для модульного теста, поэтому вы должны сократить время выполнения, применяя различные правила и методы, например, не выполнять подключение к базе данных в модульных тестах (по определению, модульные тесты изолированы) или выполнять инициализацию дорогих объектов в блок @Before.

Выберите хорошие имена для модульных тестов . Имя теста может быть сколько угодно, но оно должно представлять, какую проверку выполняет тест. Например, если мне нужно протестировать конструктор по умолчанию для класса Account, я назову его defaultConstructorTest . Еще один полезный совет по выбору названия теста — написание логики теста перед тем, как называть тест. Разрабатывая тест, вы понимаете, что происходит внутри него, в результате составление имени становится легче.

Модульные тесты должны быть предсказуемы . Это самое очевидное требование. Я объясню это на примере. Чтобы проверить операцию перевода денег (с 5% комиссией), вы должны знать, какую сумму вы отправляете и сколько вы получаете в качестве вывода. Этот тестовый сценарий может быть реализован как отправка 100 $ и получение 95 $.

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

блок-тесты-принципы

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

Основы тестовой техники проектирования

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

Во-первых, давайте представим, что мы можем отправить только положительное целое количество денег. Также мы не можем отправить более 1000. Это можно представить как:

1
0 < amount <= 1000; amount in integer

Все наши тестовые сценарии можно разделить на две группы: положительные и отрицательные. Первый предназначен для тестовых данных, которые разрешены системой и приводят к успешным результатам. Второй — для так называемых «сценариев отказов», когда мы используем неподходящие данные для взаимодействия с системой.

В соответствии с классами техники эквивалентности мы можем выбрать одно случайное целое число из диапазона (0; 1000]. Пусть это будет 500. Поскольку система работает для 500, она должна работать нормально для всех целых чисел из диапазона. Таким образом, 500 является допустимое значение. Также мы можем выбрать недопустимый ввод из диапазона. Это может быть любое число с плавающей запятой, например 125.50

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

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

  • (1, 500, 1000) — для позитивных сценариев
  • (0, 125.50, 1001) — для негативных сценариев

Резюме

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

Будьте уверены, что все тесты будут зелеными 🙂

Ссылка: Введение в Java TDD — часть 1 от нашего партнера по JCG Алексея Зволинского в блоге заметок Фрузенштейна .