Вам знакома басня про лебедя, рака и щуку? Три совершенно разных существа тянули воз: лебедь пытался взлетать с ним, рак пятился с повозкой назад, а щука упорно тащила телегу на дно. Единства между ними не произошло, поэтому затея была обречена на провал — воз так и не сдвинулся с места.
Так аллегорично можно описать стандартный рабочий процесс, в котором заказчик хочет одно, руководитель проекта понимает задачу по-другому, а специалисты (дизайнеры, программисты, тестировщики) все делают по-своему… Иногда такие проекты доводят до конца. Однако зачастую они, как повозка из упомянутой басни, остаются на месте.
Чтобы в своей профессии вы не повторяли судьбу героев басни, были созданы методологии тестирования Test Driven Development (TDD) и Behavior Driven Development (BDD). Давайте разберемся, как они работают.
TDD и BDD: основные отличия модульного и интеграционного тестирования
Test Driven Development (TDD) — это разработка, основанная на тестировании. Предположим, вы получаете от заказчика запрос на добавление в разрабатываемый продукт нового функционала. Под этот запрос составляется техническая документация в виде тестов: в них согласованы и записаны новые требования, которые заказчик предъявляет к продукту.
Тестирование в TDD происходит через итерации (циклы) и соответствует такому порядку:
- сперва написанные тесты прогоняются через существующий код, на этом этапе новые тесты упадут (failing tests);
- под упавшие тесты разработчик пишет новый код, который покроет необходимый функционал для теста, заставив его проходить;
- когда тесты с новым кодом будут пройдены с положительным результатом, можно приступать к рефакторингу — правке кода, в котором из него убирается все лишнее.
Все эти шаги TDD-разработки будут повторяться энное количество раз, пока специалисты не приведут код в соответствие с документацией и не удостоверятся в работоспособности новой функции.
Методология TDD относится к юнит-тестированию (модульному тестированию) и позволяет проверять отдельно взятые части продукта. Чаще всего TDD пишут сами разработчики, тесты реализуются в виде программного кода.
Но как протестировать не отдельный модуль продукта, а сложный сценарий с большим количеством условий и переменных?
В этом случае прибегают к использованию методологии Behavior Driven Development — разработке на основе поведения. В отличие от TDD, этот подход строится на написании нескольких пользовательских сценариев, под которые составляются тесты. BDD позволяет «предугадать», как поведет себя пользователь, используя продукт в соответствии с требованиями, которые записаны в технической документации. Порядок прохождения тестов схож с TDD. Единственное отличие — перед прохождением отдельных тестов формулируется ряд предварительных условий (сценариев), при которых они должны быть пройдены.
Требования для BDD обычно составляет группа экспертов и не в виде программного кода, а словесно — на языке, понятном всем участникам проекта.
Behavior Driven Development относится к методам интеграционного тестирования. Оно позволяет понять, правильно ли взаимодействуют друг с другом отдельно взятые части программы.
Подход также эффективен в end-to-end-тестировании (Е2Е) и дает программистам представление о том, как функционирует вся разрабатываемая ими система. Получается, несмотря на то, что BDD и является расширением TDD-методологии, они имеют разное предназначение и реализуются разным набором инструментов.
В общем, мы друг друга поняли
Методологии BDD- и TDD- тестирования помогают достичь взаимопонимания между заказчиком продукта и всеми участниками, задействованными в его реализации.
Четкое следование прописанным заранее спецификациям позволяет избежать подводных камней в виде неоговоренных сценариев или разрозненной трактовки функционала продукта разными специалистами.
Весомое преимущество таких подходов в тестировании — отсутствие невалидных (недостоверных) сценариев. Каждый участник проекта еще на стадии проектирования может увидеть нереализуемые функции и рассказать об этом коллегам. Так можно исключить даже саму возможность создания неэффективного и бесполезного кода.
Еще один плюс — наличие технической документации. TDD и BDD предполагают четко структурированную документацию, которая помогает быстрее адаптироваться новичкам, зашедшим в проект, уже на этапе производства.
Разработка больше не зависит от человеческого фактора: чтобы понять, что происходило в проекте с момента его создания, достаточно открыть нужный файл.
Как составлять тесты?
Давайте подытожим кратким списком рекомендаций по составлению тестов:
- в процессе создания и разработки новых сценариев для BDD-тестирования должны участвовать все члены команды и заказчик;
- описывайте желаемое поведение в сценариях, руководствуясь уже существующими спецификациями поведения (behavioral specifications);
- не пытайтесь проверить в одном тесте сразу несколько сценариев или функций — это перегружает его;
- следите за тем, чтобы модульное тестирование всегда выполнялось быстро (здесь стандарт — миллисекунды);
- юнит-тесты должны быть самостоятельными и не иметь внешних зависимостей от баз данных, файловых систем;
- тесты предназначены не только для проверки кода, но и для ведения технической документации;
- void-методы, которые не возвращают никаких данных, тоже надо тестировать.
Инструменты для реализации юнит-тестирования
Юнит-тестирование на Java осуществляется при помощи фреймворка JUnit. Он относится к семейству фреймворков xUnit и используется преимущественно для Test-Driven- и Behavior-Driven-разработки. На сегодня JUnit 5 — самая свежая версия фреймворка, которая совместима с версиями Java 8 и выше.
Это значит, что фреймворк поддерживает Stream API, лямбды, функциональные интерфейсы и массу других «плюшек», которые таит в себе Java 8+.
Характерным отличием JUnit 5 от своей предыдущей версии является возможность запускать сразу несколько раннеров для одного и того же класса (JUnit4 был способен выполнять только один класс-раннер). JUnit 5 состоит из трех отдельных пакетов, которые можно подключать независимо друг от друга:
- JUnit Platform — предоставляет инструменты для обработки тестов на JUnit для JVM (Java Virtual Machine);
- JUnit Vintage — обеспечивает обратную совместимость с тестами, разработанными на предыдущих версиях JUnit;
- Junit Jupiter — объединяет в себе программную и экстеншн-модели для написания тестов с использованием всего функционала пятой версии JUnit.
Экстеншн-модели и аннотации в JUnit 5
Экстеншн-модель в JUnit Jupiter — это разновидность совершенно нового API, позволяющая расширять функционал отдельного теста и добавлять новые условия для работы с ним. Для работы с экстеншн-моделью существуют экстеншн-поинты.
Существует пять основных видов экстеншн-поинтов:
- постобработка тестового экземпляра — расширение функционирует с помощью интерфейса реализации TestInstancePostProcessor и может быть выполнено после того, как был создан хотя бы один экземпляр теста;
- условное выполнение теста — расширение на базе интерфейса ExecutionCondition, которое контролирует запуск теста по условию;
- обратные вызовы жизненного цикла — набор расширений, связанных с событиями в жизненном цикле теста;
- разрешение параметра — расширение, которое определяет получение параметра в конструкторе или методе теста во время выполнения ParameterResolver;
- обработка исключений — определяет поведение теста при обнаружении определенного типа исключений. Реализован интерфейсом TestExecutionExceptionHandler.
Помимо интерфейсов в JUnit5 представлен довольно обширный список аннотаций. Рассмотрим некоторые из них:
- @RepeatedTest — повторяющиеся тесты. Разновидность тестов, которые выполняются энное количество раз, а описываются всего один. Повторяющиеся тесты довольно легко настроить при помощи таких параметров как: имя (name), и желаемое количество повторений теста (value).
- @ParametrizedTest — параметризированный тест. Позволяет запускать тест несколько раз, передавая ему разные аргументы. При выполнении этого теста каждый вызов будет обрабатываться отдельно.
- @TestTemplate — метод, предназначенный для многократного вызова тестов в зависимости от количества контекстов, возвращаемых зарегистрированными провайдерами. @TestTemplate правильно рассматривать не как отдельный тест, а как шаблон для целого ряда тестовых случаев.
- @TestFactory — сам по себе метод не является тестом в классическом понимании, а скорее — фабрикой, а отдельные вызовы (динамические тесты) — продуктами фабрики. Динамический тест генерируется в рантайме фабричными методами. Методы @TestFactory не должны быть private или static и могут дополнительно объявлять параметры, которые должны быть обработаны, при помощи @ParameterResolvers.
- @TempDir — встроенный экстеншн, который используется для создания и очистки временного каталога для одного или всех тестов в классе. Чтобы им воспользоваться, нужно аннотировать неприватное поле типа java.nio.file.Path или java.io.File при помощи @TempDir.
В JUnit пятой версии также расширен функционал Assertions API. Например, теперь вы сможете работать с методом assertAll, который построен на использовании лямбд. Он позволяет производить групповые проверки: каждая следующая проверка выполняется только в том случае, если предыдущая верна:
@Test void groupedAssertions() { // In a grouped assertion all assertions are executed, and all // failures will be reported together. assertAll("person", () -> assertEquals("Jane", person.getFirstName()), () -> assertEquals("Doe", person.getLastName()) ); } @Test void dependentAssertions() { // Within a code block, if an assertion fails the // subsequent code in the same block will be skipped. assertAll("properties", () -> { String firstName = person.getFirstName(); assertNotNull(firstName); // Executed only if the previous assertion is valid. assertAll("first name", () -> assertTrue(firstName.startsWith("J")), () -> assertTrue(firstName.endsWith("e")) ); }, () -> { // Grouped assertion, so processed independently // of results of first name assertions. String lastName = person.getLastName(); assertNotNull(lastName); // Executed only if the previous assertion is valid. assertAll("last name", () -> assertTrue(lastName.startsWith("D")), () -> assertTrue(lastName.endsWith("e")) ); } ); }
JUnit 5 также позволяет контролировать выполнение теста в зависимости от внешних условий. Вы можете выбирать ОС, на которой будете проводить тестирование, а также версию Java, на которой будет работать тест. Контролировать можно и то, при каких системных настройках будет запущен тест.
Также JUnit 5 помогает создавать свои собственные аннотации. Для этого необходимо указать @Target (ElementType.METHOD) для нового интерфейса и затем перечислить все аннотации, которые должны сработать при подключении вашей аннотации. Вот несколько примеров таких аннотаций:
@EnabledOnOs({ LINUX, MAC }) @DisabledOnOs(WINDOWS) @EnabledOnJre(JAVA_8) @EnabledOnJre({ JAVA_9, JAVA_10 }) @EnabledForJreRange(min = JAVA_9, max = JAVA_11) @DisabledOnJre(JAVA_9) @DisabledForJreRange(min = JAVA_9, max = JAVA_11) @EnabledIfSystemProperty(named = "os.arch", matches = ".*64.*") @DisabledIfSystemProperty(named = "ci-server", matches = "true") @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Test @EnabledOnOs(MAC) @interface TestOnMac { } @TestOnMac
Тестируй себя сам
Для любого разработчика очень важно уметь самостоятельно применять методы юнит-тестирования. Такой подход позволяет на ранних этапах уловить непонятные аспекты ТЗ до того, как будет реализован код. Помимо этого, TDD- и BDD- тестирование обеспечивает постоянную коммуникацию внутри команды: и исполнитель, и заказчик, и руководитель всегда будут находиться в одной плоскости понимания проекта. Именно в таком единстве всех участников процесса и заключается главное преимущество использования интеграционного и модульного тестирования.
Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.
Сообщить об опечатке
Текст, который будет отправлен нашим редакторам: