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:
- Start with conversations: The Three Amigos discussion is more important than the tools
- Write declaratively: Focus on what, not how
- Choose the right tool: Cucumber for JVM/JS, SpecFlow for .NET, Behave for Python
- Maintain living documentation: Let your tests serve as always-up-to-date docs
- Integrate with CI/CD: Automate execution and reporting
- 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.