Behavior-Driven Development (BDD) (как обсуждается в Cucumber BDD Automation: Complete Guide to Behavior-Driven Development Testing) устраняет разрыв между бизнес-требованиями и автоматизацией тестирования, создавая общее понимание между владельцами продукта, разработчиками и тестировщиками. Это подробное руководство исследует, как эффективно внедрить BDD (как обсуждается в Gauge Framework Guide: Language-Independent BDD Alternative to Cucumber) от начальных требований до интеграции с CI/CD.
Понимание BDD: больше, чем просто тестирование
BDD часто неправильно понимается как просто “тесты, написанные на простом английском”. На самом деле это совместная практика, которая меняет то, как команды думают о программном обеспечении и доставляют его.
Три Амиго
BDD сосредоточен на разговоре “Трех Амиго”:
- Product Owner/Business Analyst: Определяет, что нужно построить
- Разработчик: Определяет, как это будет построено
- Тестировщик: Исследует, что может пойти не так
Эти разговоры происходят до начала кодирования, в результате чего появляются конкретные примеры, которые становятся автоматизированными тестами.
Example Mapping
Перед написанием сценариев используйте Example Mapping для исследования требований:
Карточка истории:
┌────────────────────────────────────┐
│ Как пользователь │
│ Я хочу снять наличные │
│ Чтобы получить деньги │
└────────────────────────────────────┘
Правила (Желтые карточки):
┌──────────────────────┐ ┌──────────────────────┐
│ Должен быть │ │ Дневной лимит: $500 │
│ достаточный баланс │ │ │
└──────────────────────┘ └──────────────────────┘
Примеры (Зеленые карточки):
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐
│ Баланс: $100 │ │ Баланс: $600 │ │ Баланс: $100 │
│ Снятие: $50 │ │ Снятие: $200 │ │ Уже снято $400 │
│ ✓ Успех │ │ Уже снято $350 │ │ сегодня │
│ │ │ сегодня │ │ Попытка снять $200 │
│ │ │ Попытка снять $200 │ │ ✗ Дневной лимит │
│ │ │ ✗ Дневной лимит │ │ │
└────────────────────┘ └────────────────────┘ └────────────────────┘
Gherkin: язык BDD
Gherkin предоставляет структурированный, читаемый формат для документирования поведения.
Базовая структура Gherkin
Feature: Снятие наличных
Как клиент
Я хочу снять наличные из банкомата
Чтобы получить доступ к своим деньгам
Background:
Given у клиента есть действительный счет
And в банкомате достаточно наличных
Scenario: Успешное снятие в пределах баланса
Given у клиента баланс $100
When клиент запрашивает $50
Then наличные должны быть выданы
And баланс должен быть $50
And должен быть предоставлен чек
Scenario: Недостаточно средств
Given у клиента баланс $30
When клиент запрашивает $50
Then снятие должно быть отклонено
And должно отображаться сообщение об ошибке "Недостаточно средств"
And наличные не должны быть выданы
Лучшие практики Gherkin
1. Пишите декларативно, а не императивно
# Плохо - Императивный (как)
Scenario: Регистрация пользователя
Given я на главной странице
When я кликаю "Зарегистрироваться"
And я заполняю "email" значением "user@example.com"
And я заполняю "password" значением "Pass123!"
And я заполняю "подтвердить password" значением "Pass123!"
And я кликаю "Создать Аккаунт"
Then я должен увидеть "Добро пожаловать"
# Хорошо - Декларативный (что)
Scenario: Регистрация пользователя
Given я новый посетитель
When я регистрируюсь с действительными учетными данными
Then я должен быть залогинен
And я должен увидеть приветственное сообщение
Декларативный стиль:
- Фокусируется на что, а не как
- Остается стабильным при изменениях UI
- Более читаем для нетехнических заинтересованных сторон
- Требует более сложных определений шагов
2. Используйте Scenario Outlines для вариаций данных
# Плохо - Повторяющиеся сценарии
Scenario: Логин с неверным паролем
Given существует пользователь с email "user@example.com"
When я логинюсь с email "user@example.com" и паролем "wrong"
Then я должен увидеть ошибку "Неверные учетные данные"
# Хорошо - Scenario Outline
Scenario Outline: Валидация логина
Given существует пользователь с email "user@example.com" и паролем "Pass123!"
When я логинюсь с email "<email>" и паролем "<password>"
Then я должен увидеть ошибку "<error>"
Examples:
| email | password | error |
| user@example.com | wrong | Неверные учетные данные |
| wrong@example.com | Pass123! | Неверные учетные данные |
| user@example.com | Pass123 | Неверные учетные данные |
| | Pass123! | Email обязателен |
| user@example.com | | Пароль обязателен |
3. Используйте Background мудро
Feature: Корзина покупок
Background:
Given я залогинен как "customer@example.com"
And моя корзина пуста
Scenario: Добавить товар в корзину
When я добавляю "iPhone 15" в корзину
Then моя корзина должна содержать 1 товар
Scenario: Добавить несколько товаров
When я добавляю "iPhone 15" в корзину
And я добавляю "AirPods Pro" в корзину
Then моя корзина должна содержать 2 товара
4. Один сценарий, одно поведение
# Плохо - Тестирование множественных поведений
Scenario: Поток пользователя
Given я залогинен
When я просматриваю свой профиль
Then я должен увидеть свое имя
When я редактирую свой профиль
And я меняю свое имя на "Новое Имя"
Then мое имя должно быть обновлено
# Хорошо - Отдельные сценарии
Scenario: Просмотр профиля
Given я залогинен
When я просматриваю свой профиль
Then я должен увидеть свою текущую информацию
Scenario: Обновление имени профиля
Given я залогинен
And я на странице своего профиля
When я меняю свое имя на "Новое Имя"
Then мой профиль должен показывать "Новое Имя"
5. Используйте теги для организации
@critical @smoke
Feature: Аутентификация
@fast
Scenario: Логин с действительными учетными данными
Given существует пользователь
When я логинюсь с действительными учетными данными
Then я должен быть залогинен
@slow @integration
Scenario: Логин с SSO
Given SSO настроен
When я логинюсь через Google
Then я должен быть залогинен
@wip
Scenario: Двухфакторная аутентификация
# В разработке
Запуск конкретных тегов:
cucumber --tags "@smoke and not @wip"
cucumber --tags "@critical or @smoke"
Cucumber: чемпион Java/JavaScript
Cucumber — наиболее широко используемый BDD-фреймворк с сильной поддержкой Java, JavaScript и Ruby.
Реализация Cucumber Java
Определения шагов:
public class AuthenticationSteps {
private User user;
private LoginPage loginPage;
private DashboardPage dashboardPage;
private String errorMessage;
@Given("существует пользователь с email {string} и паролем {string}")
public void existsUserWithEmailAndPassword(String email, String password) {
user = UserFactory.createUser(email, password);
userRepository.save(user);
}
@When("я логинюсь с email {string} и паролем {string}")
public void iLoginWithEmailAndPassword(String email, String password) {
loginPage = new LoginPage(driver);
try {
dashboardPage = loginPage.login(email, password);
} catch (LoginException e) {
errorMessage = e.getMessage();
}
}
@Then("я должен быть залогинен")
public void iShouldBeLoggedIn() {
assertThat(dashboardPage).isNotNull();
assertThat(sessionManager.isLoggedIn()).isTrue();
}
@Then("я должен увидеть ошибку {string}")
public void iShouldSeeAnError(String expectedError) {
assertThat(errorMessage).contains(expectedError);
}
@After
public void cleanup() {
if (user != null) {
userRepository.delete(user);
}
sessionManager.logout();
}
}
Таблицы данных для сложных данных:
Scenario: Зарегистрировать пользователя с профилем
When я регистрируюсь со следующими деталями:
| email | name | age | city |
| john@example.com | John Doe | 30 | New York |
Then пользователь должен быть создан
@When("я регистрируюсь со следующими деталями:")
public void iRegisterWithTheFollowingDetails(DataTable dataTable) {
Map<String, String> data = dataTable.asMap(String.class, String.class);
User user = User.builder()
.email(data.get("email"))
.name(data.get("name"))
.age(Integer.parseInt(data.get("age")))
.city(data.get("city"))
.build();
registrationService.register(user);
}
Cucumber JavaScript (с Playwright)
// features/support/world.js
const { setWorldConstructor, Before, After } = require('@cucumber/cucumber') (как обсуждается в [Serenity BDD Integration: Living Documentation and Advanced Test Reporting](/blog/serenity-bdd-integration));
const { chromium } = require('playwright');
class CustomWorld {
async init() {
this.browser = await chromium.launch();
this.context = await this.browser.newContext();
this.page = await this.context.newPage();
}
async cleanup() {
await this.page?.close();
await this.context?.close();
await this.browser?.close();
}
}
setWorldConstructor(CustomWorld);
Before(async function() {
await this.init();
});
After(async function() {
await this.cleanup();
});
// features/step_definitions/authentication.steps.js
const { Given, When, Then } = require('@cucumber/cucumber');
const { expect } = require('@playwright/test');
Given('существует пользователь с email {string}', async function(email) {
this.user = await createUser({ email, password: 'Test123!' });
});
When('я логинюсь с email {string} и паролем {string}',
async function(email, password) {
await this.page.goto('http://localhost:3000/login');
await this.page.fill('[name="email"]', email);
await this.page.fill('[name="password"]', password);
await this.page.click('button[type="submit"]');
}
);
Then('я должен быть залогинен', async function() {
await this.page.waitForURL('**/dashboard');
const userMenu = await this.page.locator('[data-testid="user-menu"]');
await expect(userMenu).toBeVisible();
});
SpecFlow: BDD для .NET
SpecFlow привносит BDD в стиле Cucumber в экосистему .NET.
Feature: Аутентификация пользователя
Как пользователь
Я хочу безопасно получить доступ к своему аккаунту
Чтобы управлять своими данными
Scenario: Логин с действительными учетными данными
Given существует пользователь с email "user@example.com"
When я логинюсь с действительными учетными данными
Then я должен быть перенаправлен на dashboard
And я должен увидеть приветственное сообщение
[Binding]
public class AuthenticationSteps
{
private readonly ScenarioContext _scenarioContext;
private readonly IUserService _userService;
private User _user;
public AuthenticationSteps(
ScenarioContext scenarioContext,
IUserService userService)
{
_scenarioContext = scenarioContext;
_userService = userService;
}
[Given(@"существует пользователь с email ""(.*)""")]
public async Task ExistsUserWithEmail(string email)
{
_user = await _userService.CreateUserAsync(new User
{
Email = email,
Password = "Test123!",
Name = "Test User"
});
_scenarioContext["User"] = _user;
}
[When(@"я логинюсь с действительными учетными данными")]
public async Task ILoginWithValidCredentials()
{
var user = _scenarioContext.Get<User>("User");
await _loginPage.LoginAsync(user.Email, "Test123!");
}
[AfterScenario]
public async Task Cleanup()
{
if (_user != null)
{
await _userService.DeleteUserAsync(_user.Id);
}
}
}
Behave: BDD для Python
Behave привносит BDD в Python с чистым, Pythonic API.
# features/authentication.feature
Feature: Аутентификация пользователя
Background:
Given приложение запущено
Scenario: Успешный логин
Given существует пользователь с email "user@example.com"
When я логинюсь с email "user@example.com" и паролем "Test123!"
Then я должен быть залогинен
And я должен увидеть свой dashboard
# features/steps/authentication_steps.py
from behave import given, when, then
from selenium.webdriver.common.by import By
@given('приложение запущено')
def step_app_running(context):
context.driver.get('http://localhost:3000')
@given('существует пользователь с email "{email}"')
def step_user_exists(context, email):
context.user = create_user(
email=email,
password='Test123!',
name='Test User'
)
context.users_to_cleanup.append(context.user.id)
@when('я логинюсь с email "{email}" и паролем "{password}"')
def step_login(context, email, password):
context.driver.get('http://localhost:3000/login')
email_field = context.driver.find_element(By.NAME, 'email')
password_field = context.driver.find_element(By.NAME, 'password')
submit_button = context.driver.find_element(
By.CSS_SELECTOR, 'button[type="submit"]'
)
email_field.send_keys(email)
password_field.send_keys(password)
submit_button.click()
@then('я должен быть залогинен')
def step_should_be_logged_in(context):
wait = WebDriverWait(context.driver, 10)
wait.until(EC.url_contains('/dashboard'))
Настройка окружения (хуки):
# features/environment.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
def before_feature(context, feature):
if 'webdriver' in feature.tags:
chrome_options = Options()
chrome_options.add_argument('--headless')
context.driver = webdriver.Chrome(options=chrome_options)
def before_scenario(context, scenario):
context.users_to_cleanup = []
def after_scenario(context, scenario):
for user_id in context.users_to_cleanup:
delete_user(user_id)
if scenario.status == 'failed' and hasattr(context, 'driver'):
screenshot_name = f"screenshots/{scenario.name}.png"
context.driver.save_screenshot(screenshot_name)
def after_feature(context, feature):
if hasattr(context, 'driver'):
context.driver.quit()
Живая документация
Одно из величайших преимуществ BDD — живая документация, которая остается синхронизированной с фактическим поведением системы.
Генерация отчетов
HTML отчеты Cucumber:
# Сгенерировать JSON Cucumber
mvn test -Dcucumber.plugin="json:target/cucumber.json"
# Сгенерировать HTML отчет
npx cucumber-html-reporter \
--jsonFile=target/cucumber.json \
--output=target/cucumber-report.html
Отчеты Allure (работает с Cucumber, SpecFlow, Behave):
# Запустить тесты с Allure
mvn test -Dcucumber.plugin="io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm"
# Сгенерировать и открыть отчет
allure serve target/allure-results
Интеграция CI/CD
Тесты BDD должны быть частью вашего пайплайна непрерывной интеграции.
GitHub Actions
# .github/workflows/bdd-tests.yml
name: BDD Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up JDK 17
uses: actions/setup-java@v3
with:
java-version: '17'
- name: Run BDD tests
run: mvn test -Dcucumber.filter.tags="@smoke"
- name: Generate Allure Report
if: always()
run: mvn allure:report
- name: Publish test results
if: always()
uses: actions/upload-artifact@v3
with:
name: test-results
path: target/cucumber-reports
Параллельное выполнение
<!-- pom.xml -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<parallel>methods</parallel>
<threadCount>4</threadCount>
<perCoreThreadCount>true</perCoreThreadCount>
</configuration>
</plugin>
Распространенные ошибки BDD и как их избежать
1. Чрезмерная спецификация
# Плохо - слишком детально, хрупко
Scenario: Поиск продуктов
Given я на главной странице
When я кликаю на иконку поиска в верхнем правом углу
And я набираю "laptop" в поле поиска с id "search-input"
And я нажимаю клавишу Enter
Then я должен увидеть спиннер загрузки на 2 секунды
And я должен увидеть 15 продуктов в сетке
# Хорошо - фокусируется на поведении
Scenario: Поиск продуктов
Given я на главной странице
When я ищу "laptop"
Then я должен увидеть релевантные результаты продуктов
2. Сценарии, зависящие друг от друга
# Плохо - сценарии зависят от предыдущих
Scenario: Создать пользователя
When я создаю пользователя "john@example.com"
Then пользователь должен быть создан
Scenario: Обновить пользователя # Предполагает, что предыдущий сценарий выполнился
When я обновляю имя пользователя "john@example.com" на "John Doe"
Then имя должно быть обновлено
# Хорошо - каждый сценарий независим
Scenario: Обновить имя пользователя
Given существует пользователь с email "john@example.com"
When я обновляю имя пользователя на "John Doe"
Then имя пользователя должно быть "John Doe"
Заключение
BDD трансформирует то, как команды сотрудничают в разработке программного обеспечения. Ключевые выводы:
- Начните с разговоров: Обсуждение Трех Амиго важнее инструментов
- Пишите декларативно: Фокусируйтесь на что, а не на как
- Выберите правильный инструмент: Cucumber для JVM/JS, SpecFlow для .NET, Behave для Python
- Поддерживайте живую документацию: Пусть ваши тесты служат всегда актуальной документацией
- Интегрируйте с CI/CD: Автоматизируйте выполнение и отчетность
- Избегайте распространенных ошибок: Держите сценарии независимыми, сфокусированными на поведении и декларативными
BDD, выполненный правильно, создает общее понимание в команде, сокращает переделки и гарантирует, что то, что строится, действительно необходимо. Сценарии становятся одновременно спецификацией и верификацией, устраняя разрыв между требованиями и тестами.
Начните с малого, сфокусируйтесь на высокоценных сценариях и постепенно расширяйте свою практику BDD. Инвестиции в четкую коммуникацию и общее понимание окупаются на протяжении всего жизненного цикла разработки.