Статьи

Важность заданного когда в модульных тестах и ​​TDD

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

Вступление

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

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

Образец теста

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

01
02
03
04
05
06
07
08
09
10
11
12
private static final String ENTERPRISE_D = "USS Enterprise (NCC-1701-D)";
 
@Test
public void shouldFindOwnShipByName() {
//given
ShipDatabase shipDatabase = new ShipDatabase(ownShipIndex, enemyShipIndex);
given(ownShipIndex.findByName("Enterprise")).willReturn(singletonList(ENTERPRISE_D));
//when
List foundShips = shipDatabase.findByName("Enterprise");
//then
assertThat(foundShips).contains(ENTERPRISE_D);
}

учитывая, когда-то

Хорошая привычка, которая существует как в методологиях разработки, основанной на тестировании, так и в поведении, является «априорным» знанием того, что будет проверено (подтверждено) в конкретном тестовом примере. Это может быть сделано более формальным способом (например, сценарии, написанные в Cucumber / Gherkin для приемочных тестов) или в свободной форме (например, специальные отмеченные пункты или просто представление о том, что следует проверять дальше). Обладая этими знаниями, должно быть довольно легко определить три важные вещи (являющиеся отдельными разделами), из которых будет состоять весь тест.

дано — подготовка

В первом разделе (называемом « given ) модульного теста необходимо создать экземпляр реального объекта, на котором будет выполняться проверенная операция. В сфокусированных модульных тестах есть только один класс, в который помещается тестируемая логика. Кроме того, другие объекты, необходимые для выполнения теста (именуемые соавторы), должны быть инициализированы как заглушки / насмешки и должным образом заглушены (при необходимости). Все соавторы также должны быть внедрены в тестируемый объект, который обычно комбинируется с созданием этого объекта (поскольку внедрение конструктора должно быть предпочтительным методом внедрения зависимости).

1
2
3
//given
ShipDatabase shipDatabase = new ShipDatabase(ownShipIndex, enemyShipIndex);
given(ownShipIndex.findByName("Enterprise")).willReturn(singletonList(ENTERPRISE_D));

когда — исполнение

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

1
2
//when
List foundShips = shipDatabase.findByName("Enterprise");

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

тогда — утверждение

Ответственность за заключительный раздел — а then — главным образом за утверждение ранее полученного результата. Он должен быть равен ожидаемому значению.

1
2
//then
assertThat(foundShips).contains(ENTERPRISE_D);

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

Ааа ака 3а — альтернативный синтаксис

Как я уже упоминал, BDD представляет собой гораздо более широкую концепцию, которая особенно удобна для написания функциональных / приемочных тестов с заранее определенными требованиями (часто) в нетехнической форме. Альтернативный синтаксис тестового разделения (с очень похожим значением для разделов) — это организовать-действовать-утверждать, часто сокращенное до ААА или 3А. Если вы вообще не используете BDD и три буквы A легче запомнить, чем GWT, то прекрасно использовать его для создания одинаковых высококачественных модульных тестов.

Тюнинг и оптимизация

Процесс согласования используемых инструментов и методологий с непрерывным процессом приобретения навыков (он же модель Дрейфуса ) был хорошо описан в книге « Прагматическое мышление и обучение: рефакторинг ваших программных продуктов» . Конечно, во многих случаях может быть удобно использовать упрощенный вариант теста с given разделом, перемещенным в раздел setup/init/before или инициализированным встроенным. То же самое может применяться к разделам « when и « then которые можно объединить (в expect раздел, особенно в параметризованных тестах). Имея некоторый опыт и беглость в написании модульных тестов, вполне допустимо использовать стенографию и оптимизацию (особенно тестирование некоторых нетривиальных случаев). Пока вся команда понимает соглашение и может помнить об основных предположениях относительно написания хороших модульных тестов.

Резюме

Основываясь на своем опыте разработки программного обеспечения и в качестве тренера, я ясно вижу, что разделение (модульных) тестов на разделы делает их короче и удобнее для чтения, особенно если в команде есть менее опытные люди. Проще заполнить 3 раздела с четко определенной ответственностью, чем понять и записать все в тестах сразу. В заключение, особенно для людей, читающих только первый и последний разделы статьи, здесь собраны краткие правила:

  • given — инициализация тестируемого объекта + создание заглушек / насмешек, заглушка и внедрение
  • when — операция для проверки в данном тесте
  • then — полученное утверждение результата + проверка фиктивности (при необходимости)

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

Авторы фотографий: Томас Собек, Openclipart, https://openclipart.org/detail/242959/old-scroll

Самореклама . Хотели бы вы быстро и эффективно улучшить свои навыки тестирования своей команды и знания Spock / JUnit / Mockito / AssertJ? Я провожу сжатое (единичное) обучение тестированию, которое может оказаться полезным.