Что пишут в блогах

Подписаться

Что пишут в блогах (EN)

Разделы портала

Онлайн-тренинги

.
Стратегии упрощения определений шагов BDD
04.06.2025 00:00

Автор: Томаш Балог (Tamás Balog)
Оригинал статьи
Перевод: Ольга Алифанова

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

В ходе моей карьеры значительная часть моей тест-автоматизации включала применение какого-либо BDD-фреймворка – например, инструменты вроде Cucumber или JBehave. Как человек, который программирует, я всегда интересовался рефакторингом, сокращающим количество стандартного или дублирующего кода – кода становится меньше, и он становится понятнее. Это включает и сокращение стандартного кода в методах определения шагов и прочем связующем коде. Как их упростить? Или вообще от них избавиться?

Возможно, вы недоумеваете, что такое связующий код. С одной стороны, он состоит из методов определения шагов – это методы, говорящие BDD-фреймворку автоматизации, что запускать, столкнувшись с шагом Given, When или Then в фича-файле Gherkin. По сути эти методы склеивают части текстовых Gherkin-файлов в выполнимый код тест-автоматизации. С другой стороны, это могут быть хуки – методы, выполняющиеся до или после фич/сценариев Gherkin.

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

Стандартные определения шагов Cucumber

Чтобы погрузить вас в контекст, начну с демонстрации, как обычно выглядит определение шага Cucumber в Java.

Используем регулярное выражение, как шаблон шага:

@When("^user attempts to log in with (.*) username and (.*) password$")
void userAttemptsToLogInWith(String username, String password) { }

Или используем выражение Cucumber, как шаблон шага:

@When("user attempts to log in with {string} username and {string} password")
void userAttemptsToLogInWith(String username, String password) { }

Важные части этого метода:

  • Одна из аннотаций @Given, @When, @Then, @And, @But помещена в метод для сообщения о типе шага и возможности распознать метод, как определение шага.
  • Шаблон шага, указанный в аннотации: так Cucumber соотносит шаг в Gherkin-файле с методом.
  • Аргументы метода, соответствующие аргументам шага Gherkin.

Теперь посмотрим, как это можно упростить или выполнить иным образом.

Определения шагов на основании лямбда-выражений Java 8

Многое улучшилось и упростилось, когда в Java 8 появились лямбда-выражения. Стали доступными альтернативные способы разработки приложений. Это в том числе включает Cucumber, а также новый способ определения шагов с лямбда-выражениями.

Для этого нужно использовать другую зависимость, а именно cucumber-java8. Когда это настроено, нужно внести изменения в классы определения шагов.

Класс должен реализовать интерфейс io.cucumber.java8 (или один из его специфичных для языка вариантов). После этого в классе определения шага станут доступны новые методы, специфичные для типа шага (Given(), When() и т. д.)

Текущие методы определения шагов (или как минимум те, которые нуждаются в упрощении) нужно сконвертировать и переместить в конструктор класса. То есть такой метод:

class LoginStepDefs {
   @When("user attempts to log in with {string} username and {string} password")
   void userAttemptsToLogInWith(String username, String password) {
      //действия по авторизации
   }
}

станет таким:

class LoginStepDefs implements En {
   LoginStepDefs() {
      When("user attempts to log in with {string} username and {string} password",
         (String username, String password) -> {
           //действия по авторизации
         });
      }
}

С точки зрения читабельности:

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

Идут споры на предмет того, что лучше – обычная форма метода или лямбда-форма. Вопрос как личных предпочтений, так и практических аргументов (например, инъекции зависимостей), и обсуждается даже возможность замены библиотеки Java 8 в Cucumber на альтернативное решение.

Бесструктурные аннотации шагов

Аннотации шагов могут вызывать вопросы:

  • Действительно ли нужно явно задавать паттерн шага?
  • Нет ли альтернативы, позволяющей все же его задать?

Давным-давно, в прошлом проекте, у нас было кастомное решение автоматизации и средство запуска тестов, слегка отличавшиеся от Cucumber и других BDD-фреймворков. Это решение пользовалось своими собственными аннотациями @Given, @When и т. д., с одним основным отличием: в них не требовалось задавать паттерн шагов. Вместо этого нужно было сформулировать имя метода определения шага так, чтобы оно использовалось в файлах Gherkin.

Пример:

@Given
void userAttemptsToLogInWithXUsernameAndYPasswordOnZ(String username, String password, Server server) { }

Вы наверняка заметили буквы в верхнем регистре - X, Y и Z. Они позволяли пользователям параметризовать эти методы, и работало это так:

  • из имени метода генерировалось регулярное выражение, а X, Y и Z заменялись группой (.*), вот так:
^user attempts to log in with (.*) username and (.*) password on (.*)$
  • затем в следующем шаге фреймворк проходил по списку поддерживаемых разрешений типов параметров и делал вот что:
    • он парсил каждый аргумент (содержимое каждой (.*) группы) в соответствующий тип, заданный в списке аргументов метода,
    • а затем вставлял получившиеся значения.

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

Преимущество тут в том, что не нужно задавать паттерн в аннотации – только в имени метода. Кода меньше, он понятнее. Но аннотации шагов все еще предоставляют атрибут для кастомного паттерна, если сгенерированного по умолчанию недостаточно.

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

@When("user attempts to log in with {string} username and {string} password")
void authenticate(String username, String password) {
   //действия по авторизации
}

Использование преимуществ кастомной интеграции IDE

Применение кастомного IDE-плагина необязательно снижает количество имеющегося тест-кода, но может как минимум повысить его понятность и качество.

Моя идея включает фичу сворачивания кода – она стандартна во многих IDE и текстовых редакторах. Код сворачивается, когда какой-то диапазон текста в документе становится скрытым (по сути, сворачивается и разворачивается), и заменяется кастомным текстом-плейсхолдером, сообщающим вам, что находится в свернутой секции.

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

 

Или же это контекстные данные, дающие ту же или почти ту же информацию, что и развернутый код, но более простым образом. Хороший пример – это сворачивание создания анонимного экземпляра объекта в Java в стиле лямбда-выражений. При таком методе:

CredentialsProvider getCredentialsFor(String userType) {
   return new CredentialsProvider() {
      @Override
      public Credentials get() {
         return credentialsService.get(userType);
      }
   };
}

свернутый код будет выглядеть так:


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

Использование паттерна шага вместо имени метода определения шага

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

Тут нужно два действия. Для начала – свернуть паттерн шага в аннотации шага в эллипсис, вот так:


Код становится чище, и у вас все еще есть информация о том, для какого типа шага (Given, When или Then) предназначен этот метод.

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


Если есть такая возможность, можно также свернуть ключевые слова public и void, и у вас получится такая «сигнатура» метода:


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

Динамическое разрешение шагов

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

В такой ситуации вы можете сделать вот что – и мы сделали это в прошлом проекте: использовать динамический парсинг и разрешение шагов. Это устраняет нужду во внедрении реальных методов определений шагов. Мы в основном пользовались этим для создания шагов валидации для тест-автоматизации web-UI, как показано ниже (верхний регистр – отсылки к Selenium WebElements и Bys).

  • Тогда НАЗВАНИЕ модуля РЕКОМЕНДОВАННЫЕ_КНИГИ должно быть "Recommended books"
  • Тогда НАЗВАНИЕ первого элемента модуля РЕКОМЕНДОВАННЫЕ_КНИГИ должно быть "Atomic Habits"
  • Тогда второй АВТОР первого элемента модуля РЕКОМЕНДОВАННЫЕ_КНИГИ должен быть “Noone”

Как можно видеть, это работает в обратном порядке. Возьмем последний пример:

  • Он находит модуль Recommended Books на странице, на которой сейчас осуществляется сценарий. РЕКОМЕНДОВАННЫЕ_КНИГИ тут сопоставляется списку WebElements.
  • Он получает первый элемент списка – книгу.
  • Он получает список АВТОРОВ, как строки. АВТОР тут соответствует объекту By.
  • Затем он получает второй элемент списка авторов.
  • И, наконец, он сравнивает найденное значение с ожидаемым – оно должно быть «Noone».

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

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

Использование фреймворков с предустановленными библиотеками шагов

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

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

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

Фреймворк

Краткое описание из документации фреймворка

Сценарии реализуются как…

Karate

“Karate – единственный инструмент с открытым исходным кодом, комбинирующий автоматизацию API, имитаторы, тестирование производительности и даже UI-автоматизацию в едином фреймворке. Синтаксис не зависит от языка… простой, читабельный синтаксис - тщательно спроектированный для HTTP, JSON, GraphQL и XML. Вы можете совмещать API и UI-автоматизацию в одном сценарии.”

Gherkin Features с собственным языковым парсером Karate (вместо Cucumber)

Vividus

“VIVIDUS – инструмент тест-автоматизации, предлагающий уже реализованное решение для тестирование популярных типов приложений.”


Это также включает тестирование БД, API (Application Programming Interface) и UI (User Interface).

JBehave Stories

Citrus YAKS

“YAKS – фреймворк, позволяющий облачно-ориентированное BDD-тестирование в Kubernetes! Облачно-ориентированное означает, что ваши тесты запускаются, как Kubernetes PODs.

Как фреймворк, YAKS предоставляет набор предопределенных шагов Cucumber, помогающих связаться с различными службами передачи сообщений (Http REST, JMS, Kafka, Knative eventing) и проверять содержимое сообщений – тела и заголовков.”

Cucumber Gherkin Features

Вывести разработку сценариев на уровень кода

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

При помощи базовой настройки теста и расширения ряда базовых классов можно внедрить шаги и сценарии «Gherkin» в обычные методы JUnit, TestNG или Spock. Таким образом у вас будет нормальный прямой доступ к используемым библиотекам проверки утверждений, заглушек и т. п., а методы тестов будут похожи на настоящие сценарии Gherkin, как и генерируемые тест-отчеты.

Ниже – шаг Given с более гранулярной реализацией (см. соответствующий раздел документации JGiven):

@Test
public void validate_recipe() {
   given().the().ingredients()
      .an().egg()
      .some().milk()
      .and().the().ingredient("flour");
 
   //другие шаги…
}

Или возьмем более продвинутый сценарий – нижеприведенный тест-метод JUnit 5. Он запускает параметризованный тест с различными входными данными. По сути он имитирует запуск Gherkin Scenario Outline с наборами данных, которые получает его таблица Examples.

Этот пример взят из раздела «Параметризованные сценарии» документации JGiven, и слегка изменен для понятности и применения наиболее распространенного подхода JUnit 5 к параметризации тестов.

@ParameterizedTest
@CsvSource({ "1, 1", "0, 2", "1, 0" })
public void coffee_is_not_served(int coffees, int euros) {
   given().a_coffee_machine()
      .and().the_coffee_costs_$_euros(2)
      .and().there_are_$_coffees_left_in_the_machine(coffees);
 
 
   when().I_insert_$_one_euro_coins(euros)
      .and().I_press_the_coffee_button();
 
 
   then().I_should_not_be_served_a_coffee();
}

Этот тест-метод сгенерирует следующий отчет – у вас будут нормальные отчеты о тестировании и живая документация, которой можно делиться с заинтересованными лицами.

Можно пойти с этим примером дальше и применить магию плагинов IDE. Используя кастомный код для сворачивания, можно еще больше приблизить тест-код к читабельности реального сценария Gherkin – например, как-то так:

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

Вообще не пользоваться BDD-фреймворками

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

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

Стоит просто держать в уме, что всегда можно писать тесты, как обычные тесты JUnit или TestNG.

Возможные варианты

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

Если вы считаете, что я упустил какие-то пути упрощения, пожалуйста, дайте мне знать.

Ресурсы

Дополнительная информация