Behavior-Driven Development (BDD) (as discussed in Cucumber BDD Automation: Complete Guide to Behavior-Driven Development Testing) bridges the gap between business requirements and test automation, creating a shared understanding across product owners, developers, and testers. This comprehensive guide explores how to effectively implement BDD (as discussed in Gauge Framework Guide: Language-Independent BDD Alternative to Cucumber) from initial requirements through CI/CD integration.

Understanding BDD: More Than Just Testing

BDD is often misunderstood as merely “tests written in plain English.” It’s actually a collaborative practice that changes how teams think about and deliver software.

The Three Amigos

BDD centers around the “Three Amigos” conversation:

  • Product Owner/Business Analyst: Defines what needs to be built
  • Developer: Determines how it will be built
  • Tester: Explores what could go wrong

These conversations happen before coding begins, resulting in concrete examples that become automated tests.

Example Mapping

Before writing scenarios, use Example Mapping to explore requirements:

Story Card:
┌────────────────────────────────────┐
│ As a user                          │
│ I want to withdraw cash            │
│ So that I can get money            │
└────────────────────────────────────┘

Rules (Yellow cards):
┌──────────────────────┐  ┌──────────────────────┐
│ Must have sufficient │  │ Daily limit: $500    │
│ balance              │  │                      │
└──────────────────────┘  └──────────────────────┘

Examples (Green cards):
┌────────────────────┐  ┌────────────────────┐  ┌────────────────────┐
│ Balance: $100      │  │ Balance: $600      │  │ Balance: $100      │
│ Withdraw: $50      │  │ Withdraw: $200     │  │ Already withdrawn  │
│ ✓ Success          │  │ Already withdrawn  │  │ $400 today         │
│                    │  │ $350 today         │  │ Try withdraw $200  │
│                    │  │ Try withdraw $200  │  │ ✗ Daily limit      │
│                    │  │ ✗ Daily limit      │  │                    │
└────────────────────┘  └────────────────────┘  └────────────────────┘

Questions (Red cards):
┌────────────────────────────┐
│ What about overdraft?      │
│ International limits?      │
└────────────────────────────┘

Gherkin: The Language of BDD

Gherkin provides a structured, readable format for documenting behavior.

Basic Gherkin Structure

Feature: Cash Withdrawal
  As a customer
  I want to withdraw cash from an ATM
  So that I can access my money

  Background:
    Given the customer has a valid account
    And the ATM has sufficient cash

  Scenario: Successful withdrawal within balance
    Given the customer has a balance of $100
    When the customer requests $50
    Then the cash should be dispensed
    And the balance should be $50
    And a receipt should be provided

  Scenario: Insufficient funds
    Given the customer has a balance of $30
    When the customer requests $50
    Then the withdrawal should be rejected
    And an error message "Insufficient funds" should be displayed
    And no cash should be dispensed

Gherkin Best Practices

1. Write Declaratively, Not Imperatively

# Bad - Imperative (how)
Scenario: User registration
  Given I am on the homepage
  When I click "Sign Up"
  And I fill in "email" with "user@example.com"
  And I fill in "password" with "Pass123!"
  And I fill in "confirm password" with "Pass123!"
  And I click "Create Account"
  Then I should see "Welcome"

# Good - Declarative (what)
Scenario: User registration
  Given I am a new visitor
  When I register with valid credentials
  Then I should be logged in
  And I should see a welcome message

The declarative style:

  • Focuses on what, not how
  • Remains stable when UI changes
  • Is more readable for non-technical stakeholders
  • Requires more sophisticated step definitions

2. Use Scenario Outlines for Data Variations

# Bad - Repetitive scenarios
Scenario: Login with invalid password
  Given a user exists with email "user@example.com"
  When I login with email "user@example.com" and password "wrong"
  Then I should see an error "Invalid credentials"

Scenario: Login with invalid email
  Given a user exists with email "user@example.com"
  When I login with email "wrong@example.com" and password "Pass123!"
  Then I should see an error "Invalid credentials"

# Good - Scenario Outline
Scenario Outline: Login validation
  Given a user exists with email "user@example.com" and password "Pass123!"
  When I login with email "<email>" and password "<password>"
  Then I should see an error "<error>"

  Examples:
    | email              | password  | error               |
    | user@example.com   | wrong     | Invalid credentials |
    | wrong@example.com  | Pass123!  | Invalid credentials |
    | user@example.com   | Pass123   | Invalid credentials |
    |                    | Pass123!  | Email required      |
    | user@example.com   |           | Password required   |

3. Use Background Wisely

# Background runs before EACH scenario
Feature: Shopping Cart

  Background:
    Given I am logged in as "customer@example.com"
    And my cart is empty

  Scenario: Add item to cart
    When I add "iPhone 15" to cart
    Then my cart should contain 1 item

  Scenario: Add multiple items
    When I add "iPhone 15" to cart
    And I add "AirPods Pro" to cart
    Then my cart should contain 2 items

Warning: Don’t put expensive operations in Background if only some scenarios need them.

4. One Scenario, One Behavior

# Bad - Testing multiple behaviors
Scenario: User workflow
  Given I am logged in
  When I view my profile
  Then I should see my name
  When I edit my profile
  And I change my name to "New Name"
  Then my name should be updated
  When I logout
  Then I should be logged out

# Good - Separate scenarios
Scenario: View profile
  Given I am logged in
  When I view my profile
  Then I should see my current information

Scenario: Update profile name
  Given I am logged in
  And I am on my profile page
  When I change my name to "New Name"
  Then my profile should show "New Name"

5. Use Tags for Organization

@critical @smoke
Feature: Authentication

  @fast
  Scenario: Login with valid credentials
    Given a user exists
    When I login with valid credentials
    Then I should be logged in

  @slow @integration
  Scenario: Login with SSO
    Given SSO is configured
    When I login via Google
    Then I should be logged in

  @wip
  Scenario: Two-factor authentication
    # Work in progress

Run specific tags:

cucumber --tags "@smoke and not @wip"
cucumber --tags "@critical or @smoke"

Cucumber: The Java/JavaScript Champion

Cucumber is the most widely used BDD framework, with strong support for Java, JavaScript, and Ruby.

Cucumber Java Implementation

Step definitions:

public class AuthenticationSteps {

    private User user;
    private LoginPage loginPage;
    private DashboardPage dashboardPage;
    private String errorMessage;

    @Given("a user exists with email {string} and password {string}")
    public void aUserExistsWithEmailAndPassword(String email, String password) {
        user = UserFactory.createUser(email, password);
        userRepository.save(user);
    }

    @Given("I am logged in as {string}")
    public void iAmLoggedInAs(String email) {
        user = userRepository.findByEmail(email);
        sessionManager.login(user);
    }

    @When("I login with email {string} and password {string}")
    public void iLoginWithEmailAndPassword(String email, String password) {
        loginPage = new LoginPage(driver);
        try {
            dashboardPage = loginPage.login(email, password);
        } catch (LoginException e) {
            errorMessage = e.getMessage();
        }
    }

    @When("I login with valid credentials")
    public void iLoginWithValidCredentials() {
        iLoginWithEmailAndPassword(user.getEmail(), user.getPassword());
    }

    @Then("I should be logged in")
    public void iShouldBeLoggedIn() {
        assertThat(dashboardPage).isNotNull();
        assertThat(sessionManager.isLoggedIn()).isTrue();
    }

    @Then("I should see an error {string}")
    public void iShouldSeeAnError(String expectedError) {
        assertThat(errorMessage).contains(expectedError);
    }

    @After
    public void cleanup() {
        if (user != null) {
            userRepository.delete(user);
        }
        sessionManager.logout();
    }
}

Dependency injection with Cucumber-Spring:

@CucumberContextConfiguration
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CucumberSpringConfiguration (as discussed in [Serenity BDD Integration: Living Documentation and Advanced Test Reporting](/blog/serenity-bdd-integration)) {

    @LocalServerPort
    private int port;

    @Before
    public void setup() {
        RestAssured.port = port;
    }
}

public class StepDefinitions {

    @Autowired
    private UserService userService;

    @Autowired
    private TestRestTemplate restTemplate;

    // Steps can now use autowired dependencies
}

Data tables for complex data:

Scenario: Register user with profile
  When I register with the following details:
    | email              | name       | age | city      |
    | john@example.com   | John Doe   | 30  | New York  |
  Then the user should be created
@When("I register with the following details:")
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);
}

// Or with list of objects
@When("I register multiple users:")
public void iRegisterMultipleUsers(DataTable dataTable) {
    List<User> users = dataTable.asList(User.class);
    users.forEach(registrationService::register);
}

Cucumber JavaScript (with Playwright)

// features/support/world.js
const { setWorldConstructor, Before, After } = require('@cucumber/cucumber');
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('a user exists with email {string}', async function(email) {
  this.user = await createUser({ email, password: 'Test123!' });
});

When('I login with email {string} and password {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('I should be logged in', async function() {
  await this.page.waitForURL('**/dashboard');
  const userMenu = await this.page.locator('[data-testid="user-menu"]');
  await expect(userMenu).toBeVisible();
});

SpecFlow: BDD for .NET

SpecFlow brings Cucumber-style BDD to the .NET ecosystem.

# Features/Authentication.feature
Feature: User Authentication
  As a user
  I want to securely access my account
  So that I can manage my data

  Scenario: Login with valid credentials
    Given a user exists with email "user@example.com"
    When I login with valid credentials
    Then I should be redirected to the dashboard
    And I should see a welcome message
// StepDefinitions/AuthenticationSteps.cs
[Binding]
public class AuthenticationSteps
{
    private readonly ScenarioContext _scenarioContext;
    private readonly IUserService _userService;
    private readonly LoginPage _loginPage;
    private User _user;
    private string _welcomeMessage;

    public AuthenticationSteps(
        ScenarioContext scenarioContext,
        IUserService userService,
        LoginPage loginPage)
    {
        _scenarioContext = scenarioContext;
        _userService = userService;
        _loginPage = loginPage;
    }

    [Given(@"a user exists with email ""(.*)""")]
    public async Task GivenAUserExistsWithEmail(string email)
    {
        _user = await _userService.CreateUserAsync(new User
        {
            Email = email,
            Password = "Test123!",
            Name = "Test User"
        });

        _scenarioContext["User"] = _user;
    }

    [When(@"I login with valid credentials")]
    public async Task WhenILoginWithValidCredentials()
    {
        var user = _scenarioContext.Get<User>("User");
        await _loginPage.LoginAsync(user.Email, "Test123!");
    }

    [Then(@"I should be redirected to the dashboard")]
    public void ThenIShouldBeRedirectedToTheDashboard()
    {
        var currentUrl = _loginPage.Driver.Url;
        currentUrl.Should().Contain("/dashboard");
    }

    [Then(@"I should see a welcome message")]
    public void ThenIShouldSeeAWelcomeMessage()
    {
        _welcomeMessage = _loginPage.GetWelcomeMessage();
        _welcomeMessage.Should().Contain("Welcome");
    }

    [AfterScenario]
    public async Task Cleanup()
    {
        if (_user != null)
        {
            await _userService.DeleteUserAsync(_user.Id);
        }
    }
}

Table handling in SpecFlow:

Scenario: Create order with multiple items
  When I create an order with items:
    | Product     | Quantity | Price |
    | iPhone 15   | 1        | 999   |
    | AirPods Pro | 2        | 249   |
  Then the order total should be $1497
[When(@"I create an order with items:")]
public void WhenICreateAnOrderWithItems(Table table)
{
    var items = table.CreateSet<OrderItem>();

    _order = new Order
    {
        CustomerId = _customer.Id,
        Items = items.ToList()
    };

    _orderService.CreateOrder(_order);
}

public class OrderItem
{
    public string Product { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

Behave: BDD for Python

Behave brings BDD to Python with a clean, Pythonic API.

# features/authentication.feature
Feature: User Authentication

  Background:
    Given the application is running

  Scenario: Successful login
    Given a user exists with email "user@example.com"
    When I login with email "user@example.com" and password "Test123!"
    Then I should be logged in
    And I should see my dashboard
# features/steps/authentication_steps.py
from behave import given, when, then
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

@given('the application is running')
def step_app_running(context):
    context.driver.get('http://localhost:3000')

@given('a user exists with 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('I login with email "{email}" and password "{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('I should be logged in')
def step_should_be_logged_in(context):
    wait = WebDriverWait(context.driver, 10)
    wait.until(EC.url_contains('/dashboard'))

@then('I should see my dashboard')
def step_see_dashboard(context):
    wait = WebDriverWait(context.driver, 10)
    dashboard = wait.until(
        EC.presence_of_element_located((By.ID, 'dashboard'))
    )
    assert dashboard.is_displayed()

Environment setup (hooks):

# features/environment.py
from selenium import webdriver
from selenium.webdriver.chrome.options import Options

def before_all(context):
    """Runs once before all features"""
    context.config = load_config()

def before_feature(context, feature):
    """Runs before each 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):
    """Runs before each scenario"""
    context.users_to_cleanup = []

def after_scenario(context, scenario):
    """Runs after each scenario"""
    # Cleanup test data
    for user_id in context.users_to_cleanup:
        delete_user(user_id)

    # Screenshot on failure
    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):
    """Runs after each feature"""
    if hasattr(context, 'driver'):
        context.driver.quit()

Table handling in Behave:

Scenario: Register multiple users
  When I register users:
    | name       | email              | role  |
    | John Doe   | john@example.com   | admin |
    | Jane Smith | jane@example.com   | user  |
  Then both users should be created
@when('I register users:')
def step_register_users(context):
    for row in context.table:
        user = create_user(
            name=row['name'],
            email=row['email'],
            role=row['role']
        )
        context.users_to_cleanup.append(user.id)

Living Documentation

One of BDD’s greatest benefits is living documentation that stays in sync with the actual system behavior.

Generating Reports

Cucumber HTML Reports:

# Generate Cucumber JSON
mvn test -Dcucumber.plugin="json:target/cucumber.json"

# Generate HTML report
npx cucumber-html-reporter \
  --jsonFile=target/cucumber.json \
  --output=target/cucumber-report.html

Allure Reports (works with Cucumber, SpecFlow, Behave):

<!-- pom.xml -->
<dependency>
    <groupId>io.qameta.allure</groupId>
    <artifactId>allure-cucumber7-jvm</artifactId>
</dependency>
# Run tests with Allure
mvn test -Dcucumber.plugin="io.qameta.allure.cucumber7jvm.AllureCucumber7Jvm"

# Generate and serve report
allure serve target/allure-results

Custom documentation generation:

# generate_docs.py
import json
from pathlib import Path

def generate_feature_docs(cucumber_json_path):
    with open(cucumber_json_path) as f:
        features = json.load(f)

    docs = ["# Test Scenarios Documentation\n"]

    for feature in features:
        docs.append(f"## {feature['name']}\n")
        docs.append(f"{feature['description']}\n")

        for element in feature['elements']:
            status = "✅" if all(s['result']['status'] == 'passed'
                               for s in element['steps']) else "❌"

            docs.append(f"### {status} {element['name']}\n")

            for step in element['steps']:
                step_status = "✓" if step['result']['status'] == 'passed' else "✗"
                docs.append(f"- {step_status} {step['keyword']}{step['name']}\n")

            docs.append("\n")

    Path('documentation.md').write_text(''.join(docs))

# Usage
generate_feature_docs('target/cucumber.json')

Publishing to Confluence/Wiki

from atlassian import Confluence

def publish_to_confluence(feature_files, confluence_space, confluence_page):
    confluence = Confluence(
        url='https://your-instance.atlassian.net',
        username='your-email@example.com',
        password='your-api-token'
    )

    content = generate_html_from_features(feature_files)

    confluence.update_page(
        page_id=confluence_page,
        title='Automated Test Scenarios',
        body=content
    )

CI/CD Integration

BDD tests should be part of your continuous integration pipeline.

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'
          distribution: 'temurin'

      - name: Cache Maven packages
        uses: actions/cache@v3
        with:
          path: ~/.m2
          key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }}

      - name: Run BDD tests
        run: mvn test -Dcucumber.filter.tags="@smoke"

      - name: Generate Allure Report
        if: always()
        run: |
          mvn allure:report
          mvn allure:serve &

      - name: Publish test results
        if: always()
        uses: actions/upload-artifact@v3
        with:
          name: test-results
          path: |
            target/cucumber-reports
            target/allure-results

      - name: Comment PR with results
        if: github.event_name == 'pull_request'
        uses: actions/github-script@v6
        with:
          script: |
            const fs = require('fs');
            const results = JSON.parse(
              fs.readFileSync('target/cucumber.json', 'utf8')
            );

            const passed = results.filter(f =>
              f.elements.every(e =>
                e.steps.every(s => s.result.status === 'passed')
              )
            ).length;

            const total = results.length;

            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## BDD Test Results\n✅ ${passed}/${total} features passed`
            });

Jenkins Pipeline

pipeline {
    agent any

    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }

        stage('Build') {
            steps {
                sh 'mvn clean compile'
            }
        }

        stage('BDD Tests - Smoke') {
            steps {
                sh 'mvn test -Dcucumber.filter.tags="@smoke"'
            }
        }

        stage('BDD Tests - Regression') {
            when {
                branch 'main'
            }
            steps {
                sh 'mvn test -Dcucumber.filter.tags="not @wip"'
            }
        }

        stage('Generate Reports') {
            steps {
                cucumber buildStatus: 'SUCCESS',
                    reportTitle: 'BDD Test Report',
                    fileIncludePattern: '**/*.json',
                    trendsLimit: 10

                allure([
                    includeProperties: false,
                    jdk: '',
                    properties: [],
                    reportBuildPolicy: 'ALWAYS',
                    results: [[path: 'target/allure-results']]
                ])
            }
        }
    }

    post {
        always {
            junit 'target/surefire-reports/*.xml'
            archiveArtifacts artifacts: 'target/cucumber-reports/**/*'
        }

        failure {
            emailext(
                subject: "BDD Tests Failed: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
                body: "Check console output at ${env.BUILD_URL}",
                to: "${env.CHANGE_AUTHOR_EMAIL}"
            )
        }
    }
}

Parallel Execution

Cucumber JUnit with parallel execution:

@RunWith(Cucumber.class)
@CucumberOptions(
    features = "src/test/resources/features",
    glue = "stepdefinitions",
    plugin = {"json:target/cucumber-report.json"},
    tags = "@smoke"
)
public class RunCucumberTest {
}
<!-- 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>

Common BDD Pitfalls and How to Avoid Them

1. Over-Specification

# Bad - too detailed, brittle
Scenario: Search for products
  Given I am on the homepage
  When I click the search icon in the top right corner
  And I type "laptop" in the search box with id "search-input"
  And I press the Enter key
  Then I should see a loading spinner for 2 seconds
  And I should see 15 products displayed in a grid layout
  And each product should have an image, title, price, and rating

# Good - focuses on behavior
Scenario: Search for products
  Given I am on the homepage
  When I search for "laptop"
  Then I should see relevant product results

2. Scenarios That Depend on Each Other

# Bad - scenarios depend on previous ones
Scenario: Create user
  When I create user "john@example.com"
  Then user should be created

Scenario: Update user # Assumes previous scenario ran
  When I update user "john@example.com" name to "John Doe"
  Then name should be updated

# Good - each scenario is independent
Scenario: Update user name
  Given a user exists with email "john@example.com"
  When I update the user's name to "John Doe"
  Then the user's name should be "John Doe"

3. Testing Implementation Instead of Behavior

# Bad - testing implementation
Scenario: Password hashing
  When a user registers with password "secret123"
  Then the password should be hashed with bcrypt
  And the hash should start with "$2a$"

# Good - testing behavior
Scenario: Secure password storage
  When a user registers with password "secret123"
  Then the password should not be stored in plain text
  And the user should be able to login with "secret123"

Conclusion

BDD transforms how teams collaborate on software development. Key takeaways:

  1. Start with conversations: The Three Amigos discussion is more important than the tools
  2. Write declaratively: Focus on what, not how
  3. Choose the right tool: Cucumber for JVM/JS, SpecFlow for .NET, Behave for Python
  4. Maintain living documentation: Let your tests serve as always-up-to-date docs
  5. Integrate with CI/CD: Automate execution and reporting
  6. Avoid common pitfalls: Keep scenarios independent, behavior-focused, and declarative

BDD done right creates a shared understanding across the team, reduces rework, and ensures that what gets built is what was actually needed. The scenarios become both specification and verification, eliminating the gap between requirements and tests.

Start small, focus on high-value scenarios, and gradually expand your BDD practice. The investment in clear communication and shared understanding pays dividends throughout the development lifecycle.