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 трансформирует то, как команды сотрудничают в разработке программного обеспечения. Ключевые выводы:

  1. Начните с разговоров: Обсуждение Трех Амиго важнее инструментов
  2. Пишите декларативно: Фокусируйтесь на что, а не на как
  3. Выберите правильный инструмент: Cucumber для JVM/JS, SpecFlow для .NET, Behave для Python
  4. Поддерживайте живую документацию: Пусть ваши тесты служат всегда актуальной документацией
  5. Интегрируйте с CI/CD: Автоматизируйте выполнение и отчетность
  6. Избегайте распространенных ошибок: Держите сценарии независимыми, сфокусированными на поведении и декларативными

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

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