Рубріки: Тестирование

Что надо знать о TDD и BDD: как тесты помогают разработчикам работать в команде и понять заказчика

Мария Головко

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

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

Чтобы в своей профессии вы не повторяли судьбу героев басни, были созданы методологии тестирования Test Driven Development (TDD) и Behavior Driven Development (BDD). Давайте разберемся, как они работают.

TDD и BDD: основные отличия модульного и интеграционного тестирования

Test Driven Development (TDD) — это разработка, основанная на тестировании. Предположим, вы получаете от заказчика запрос на добавление в разрабатываемый продукт нового функционала. Под этот запрос составляется техническая документация в виде тестов: в них согласованы и записаны новые требования, которые заказчик предъявляет к продукту.

Тестирование в TDD происходит через итерации (циклы) и соответствует такому порядку:

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

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

Шаги TDD-разработки

Методология TDD относится к юнит-тестированию (модульному тестированию) и позволяет проверять отдельно взятые части продукта. Чаще всего TDD пишут сами разработчики, тесты реализуются в виде программного кода.

Но как протестировать не отдельный модуль продукта, а сложный сценарий с большим количеством условий и переменных? 

В этом случае прибегают к использованию методологии Behavior Driven Development — разработке на основе поведения. В отличие от TDD, этот подход строится на написании нескольких пользовательских сценариев, под которые составляются тесты. BDD позволяет «‎предугадать», как поведет себя пользователь, используя продукт в соответствии с требованиями, которые записаны в технической документации. Порядок прохождения тестов схож с TDD. Единственное отличие — перед прохождением отдельных тестов формулируется ряд предварительных условий (сценариев), при которых они должны быть пройдены.

Сравненение BDD и TDD / TestLodge

Требования для 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 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- тестирование обеспечивает постоянную коммуникацию внутри команды: и исполнитель, и заказчик, и руководитель всегда будут находиться в одной плоскости понимания проекта. Именно в таком единстве всех участников процесса и заключается главное преимущество использования интеграционного и модульного тестирования.

If you have found a spelling error, please, notify us by selecting that text and pressing Ctrl+Enter.

Останні статті

Токсичные коллеги. Как не стать одним из них и прекратить ныть

В благословенные офисные времена, когда не было большой войны и коронавируса, люди гораздо больше общались…

07.12.2023

Делать что-то впервые всегда очень трудно. Две истории о начале карьеры PM

Вот две истории из собственного опыта, с тех пор, когда только начинал делать свою карьеру…

04.12.2023

«Тыжпрограммист». Как люди не из ІТ-отрасли обесценивают профессию

«Ты же программист». За свою жизнь я много раз слышал эту фразу. От всех. Кто…

15.11.2023

Почему чат GitHub Copilot лучше для разработчиков, чем ChatGPT

Отличные новости! Если вы пропустили, GitHub Copilot — это уже не отдельный продукт, а набор…

13.11.2023

Как мы используем ИИ и Low-Code технологии для разработки IT-продукта

Несколько месяцев назад мы с командой Promodo (агентство инвестировало в продукт более $100 000) запустили…

07.11.2023

Университет или курсы. Что лучше для получения IT-образования

Пару дней назад прочитал сообщение о том, что хорошие курсы могут стать альтернативой классическому образованию.…

19.10.2023