Behavior-Driven Development
Behavior-Driven Development
Overview
Behavior-Driven Development (BDD) is a collaborative software development approach that extends Test-Driven Development by emphasizing communication between developers, testers, and business stakeholders. BDD uses natural language constructs (Given-When-Then) to describe system behavior in terms of business value, creating a shared understanding of requirements before implementation begins. This approach bridges the gap between technical and non-technical team members, resulting in executable specifications that serve as both documentation and automated tests.
BDD transforms requirements into living documentation that remains accurate throughout the software lifecycle. By focusing on behavior rather than implementation, BDD helps teams build the right thing, not just build things right.
Key Concepts
The Three Amigos
┌─────────────────────────────────────────────────────────┐
│ The Three Amigos Session │
├─────────────────────────────────────────────────────────┤
│ │
│ 👔 Business Analyst 🧪 Tester 💻 Developer │
│ (What to build) (Edge cases) (How to build)│
│ │
│ Collaborate to define: │
│ • Feature scope & acceptance criteria │
│ • Concrete examples & edge cases │
│ • Gherkin scenarios (Given-When-Then) │
│ • Definition of Done │
│ │
└─────────────────────────────────────────────────────────┘
BDD Workflow
Discovery → Formulation → Automation
↓ ↓ ↓
Example Gherkin Code
Mapping Scenarios (Cucumber)
↓ ↓ ↓
Business Shared Executable
Rules Understanding Specification
Example Mapping Process
| Step | Activity | Output | Participants |
|---|---|---|---|
| 1. Story Card | Review user story | Understanding of goal | All three amigos |
| 2. Rules | Define business rules | Yellow cards (rules) | BA leads, all contribute |
| 3. Examples | Create concrete examples | Blue cards (examples) | Tester leads, all contribute |
| 4. Questions | Identify unknowns | Red cards (questions) | All identify gaps |
| 5. Scenarios | Convert to Gherkin | Executable specs | Developer implements |
BDD vs TDD
| Aspect | TDD | BDD |
|---|---|---|
| Focus | Internal quality | External behavior |
| Language | Technical (assertions) | Natural (Given-When-Then) |
| Audience | Developers | Entire team |
| Scope | Unit tests | Acceptance tests |
| When | Implementation phase | Discovery & implementation |
| Artifact | Test code | Living documentation |
Benefits of BDD
Business Value:
- Ensures software meets actual needs
- Reduces rework from misunderstood requirements
- Provides traceability from features to tests
Team Collaboration:
- Creates shared language between technical and business
- Surfaces ambiguities early
- Improves estimation accuracy
Technical Benefits:
- Living documentation stays up-to-date
- Regression safety for refactoring
- Executable specifications guide development
Best Practices
1. Start with Discovery, Not Implementation
Conduct Three Amigos sessions before writing code. Use Example Mapping to surface ambiguities and edge cases early.
2. Write Scenarios in Business Language
Avoid technical jargon in Gherkin. Scenarios should be readable by business stakeholders without translation.
3. Focus on Business Behavior, Not UI Details
Describe what happens, not how. UI changes shouldn’t break scenarios.
4. Keep Scenarios Independent and Isolated
Each scenario should run independently. No shared state between scenarios.
5. Maintain Living Documentation
Update scenarios when behavior changes. Scenarios are documentation, not artifacts frozen in time.
Code Examples
Example 1: BDD Discovery Process (Example Mapping)
// User Story
// As a customer, I want to apply discount codes to my order
// so that I can save money on my purchase
// Three Amigos Session Notes:
// RULES (Yellow Cards):
// 1. Discount codes are case-insensitive
// 2. Only one discount code per order
// 3. Expired codes cannot be applied
// 4. Discount codes can be percentage or fixed amount
// 5. Minimum order value may be required
// EXAMPLES (Blue Cards):
// Example 1: Valid percentage discount
// Given an order total of $100
// When I apply discount code "SAVE20"
// Then my total should be $80
// Example 2: Expired discount code
// Given discount code "EXPIRED" expired yesterday
// When I apply code "EXPIRED"
// Then I should see error "This discount code has expired"
// Example 3: Multiple discount codes
// Given I've already applied code "SAVE10"
// When I try to apply code "SAVE20"
// Then I should see error "Only one discount code allowed per order"
// QUESTIONS (Red Cards):
// Q1: What happens if discount reduces total below $0?
// Q2: Can discount codes be combined with sale prices?
// Q3: Who creates/manages discount codes?
// Q4: Are there usage limits (max uses per customer)?
// OUTCOME: Ready to write scenarios for examples 1-3
// Need to research Q1-Q4 before implementation
Example 2: From Example Mapping to Gherkin Scenarios
# File: src/test/resources/features/discount-codes.feature
Feature: Apply Discount Codes
As a customer
I want to apply discount codes to my order
So that I can save money on my purchase
Background:
Given the following discount codes exist:
| code | type | amount | expiryDate |
| SAVE20 | percentage | 20 | 2026-12-31 |
| FLAT10 | fixed | 10 | 2026-12-31 |
| EXPIRED | percentage | 15 | 2025-01-01 |
# ✅ GOOD - Focuses on behavior, not implementation
Scenario: Apply valid percentage discount code
Given I have an order with total $100.00
When I apply discount code "SAVE20"
Then my order total should be $80.00
And the discount code "SAVE20" should be applied
# ✅ GOOD - Tests business rule clearly
Scenario: Reject expired discount code
Given I have an order with total $100.00
When I apply discount code "EXPIRED"
Then I should see error message "This discount code has expired"
And my order total should be $100.00
# ✅ GOOD - Tests constraint clearly
Scenario: Cannot apply multiple discount codes
Given I have an order with total $100.00
And I have applied discount code "SAVE20"
When I apply discount code "FLAT10"
Then I should see error message "Only one discount code allowed per order"
And my order total should be $80.00
# ✅ GOOD - Uses Scenario Outline for multiple examples
Scenario Outline: Discount codes are case-insensitive
Given I have an order with total $100.00
When I apply discount code "<code>"
Then my order total should be $80.00
Examples:
| code |
| SAVE20 |
| save20 |
| Save20 |
| sAvE20 |
# ❌ BAD - Too focused on UI implementation
# Scenario: Enter discount code in checkout
# Given I am on the checkout page
# And I see a text field with id "discount-code-input"
# When I type "SAVE20" into the field with id "discount-code-input"
# And I click the button with id "apply-discount-btn"
# Then the element with id "total-amount" should display "$80.00"
Example 3: Implementing Step Definitions (Cucumber-JVM)
// Step definitions connect Gherkin to Java code
@SpringBootTest
public class DiscountCodeSteps {
private Order order;
private DiscountService discountService;
private String errorMessage;
@Autowired
public DiscountCodeSteps(DiscountService discountService) {
this.discountService = discountService;
}
// ✅ GOOD - Step definition focused on domain concepts
@Given("I have an order with total ${double}")
public void iHaveAnOrderWithTotal(double total) {
order = new Order();
order.addItem(new OrderItem("Product", Money.usd(total)));
}
@Given("the following discount codes exist:")
public void theFollowingDiscountCodesExist(DataTable dataTable) {
List<Map<String, String>> codes = dataTable.asMaps();
for (Map<String, String> row : codes) {
DiscountCode code = new DiscountCode(
row.get("code"),
DiscountType.valueOf(row.get("type").toUpperCase()),
new BigDecimal(row.get("amount")),
LocalDate.parse(row.get("expiryDate"))
);
discountService.saveDiscountCode(code);
}
}
@When("I apply discount code {string}")
public void iApplyDiscountCode(String code) {
try {
discountService.applyDiscount(order, code);
} catch (DiscountException e) {
errorMessage = e.getMessage();
}
}
@Then("my order total should be ${double}")
public void myOrderTotalShouldBe(double expectedTotal) {
assertThat(order.getTotal().getAmount())
.isEqualTo(new BigDecimal(expectedTotal));
}
@Then("I should see error message {string}")
public void iShouldSeeErrorMessage(String expectedError) {
assertThat(errorMessage).isEqualTo(expectedError);
}
@Then("the discount code {string} should be applied")
public void theDiscountCodeShouldBeApplied(String code) {
assertThat(order.getAppliedDiscountCode()).isEqualTo(code);
}
}
// ❌ BAD - Step definitions tied to UI implementation
// @When("I type {string} into the field with id {string}")
// public void iTypeIntoField(String text, String fieldId) {
// WebElement field = driver.findElement(By.id(fieldId));
// field.sendKeys(text);
// }
// Problem: Brittle, breaks when UI changes, not reusable
// ✅ GOOD - Reusable, UI-agnostic step
// @When("I apply discount code {string}")
// Can work with UI, API, or direct service call
Example 4: BDD with Spring Integration
// Configuration for BDD tests with Spring Boot
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@CucumberContextConfiguration
public class CucumberSpringConfiguration {
// Spring context for Cucumber tests
}
// Feature-specific test runner
@RunWith(Cucumber.class)
@CucumberOptions(
features = "src/test/resources/features",
glue = {"com.example.steps", "com.example.config"},
plugin = {
"pretty",
"html:target/cucumber-reports/cucumber.html",
"json:target/cucumber-reports/cucumber.json",
"junit:target/cucumber-reports/cucumber.xml"
},
tags = "not @wip" // Exclude work-in-progress scenarios
)
public class RunCucumberTests {
}
// Hooks for setup/teardown
public class CucumberHooks {
@Autowired
private OrderRepository orderRepository;
@Autowired
private DiscountCodeRepository discountCodeRepository;
// ✅ GOOD - Clean state between scenarios
@Before
public void beforeScenario() {
// Scenarios should be independent
orderRepository.deleteAll();
discountCodeRepository.deleteAll();
}
@After
public void afterScenario(Scenario scenario) {
// Clean up resources
if (scenario.isFailed()) {
// Log additional debugging info for failed scenarios
System.out.println("Scenario failed: " + scenario.getName());
}
}
@BeforeStep
public void beforeStep() {
// Can add step-level logging if needed
}
}
// ✅ GOOD - Domain model with behavior
public class Order {
private List<OrderItem> items = new ArrayList<>();
private String appliedDiscountCode;
private Money discount = Money.ZERO;
public Money getTotal() {
Money subtotal = items.stream()
.map(OrderItem::getPrice)
.reduce(Money.ZERO, Money::add);
return subtotal.subtract(discount);
}
public void applyDiscount(String code, Money discountAmount) {
if (appliedDiscountCode != null) {
throw new DiscountException("Only one discount code allowed per order");
}
this.appliedDiscountCode = code;
this.discount = discountAmount;
}
}
// ✅ GOOD - Service with clear business logic
@Service
public class DiscountService {
@Autowired
private DiscountCodeRepository repository;
public void applyDiscount(Order order, String code) {
DiscountCode discountCode = repository.findByCode(code.toUpperCase())
.orElseThrow(() -> new DiscountException("Invalid discount code"));
if (discountCode.isExpired()) {
throw new DiscountException("This discount code has expired");
}
Money discountAmount = discountCode.calculateDiscount(order.getSubtotal());
order.applyDiscount(code, discountAmount);
}
}
Example 5: BDD Anti-Patterns and Solutions
# ❌ ANTI-PATTERN 1: Imperative scenarios (UI-focused)
Scenario: Apply discount code (BAD)
Given I open the browser
And I navigate to "http://localhost:8080/checkout"
And I enter "test@example.com" in the "email" field
And I enter "Product123" in the "product" field
And I click "Add to Cart"
And I click "Checkout"
And I enter "SAVE20" in the "discount-code" field
And I click "Apply"
Then I should see "$80.00" in the "total" field
# ✅ CORRECT: Declarative scenarios (behavior-focused)
Scenario: Apply discount code (GOOD)
Given I have an order for "Product123" totaling $100.00
When I apply discount code "SAVE20"
Then my order total should be $80.00
# ❌ ANTI-PATTERN 2: Incidental details obscuring intent
Scenario: Process order (BAD)
Given a customer with id 12345
And the customer has email "test@example.com"
And the customer has address "123 Main St, Anytown, ST 12345"
And the customer has phone "555-1234"
And an order with id 67890
And the order has item "Widget" with SKU "WDG-001" quantity 2 price $25.00
And the order has item "Gadget" with SKU "GDG-002" quantity 1 price $50.00
When the order is submitted
Then the order status should be "CONFIRMED"
# ✅ CORRECT: Focus on what matters for this scenario
Scenario: Process order (GOOD)
Given I have an order totaling $100.00
When I submit the order
Then the order status should be "CONFIRMED"
# ❌ ANTI-PATTERN 3: Testing multiple behaviors in one scenario
Scenario: Complete order workflow (BAD)
Given I have an empty cart
When I add "Widget" to cart
Then my cart should contain 1 item
When I add "Gadget" to cart
Then my cart should contain 2 items
When I apply discount code "SAVE10"
Then my total should be $90.00
When I submit the order
Then I should receive confirmation email
And order status should be "PENDING"
And inventory should be decremented
# ✅ CORRECT: One scenario per behavior
Scenario: Add items to cart
Given I have an empty cart
When I add "Widget" and "Gadget" to cart
Then my cart should contain 2 items
Scenario: Apply discount to order
Given I have an order totaling $100.00
When I apply discount code "SAVE10"
Then my total should be $90.00
Scenario: Submit order sends confirmation
Given I have a valid order
When I submit the order
Then I should receive confirmation email
# ❌ ANTI-PATTERN 4: Technical language in scenarios
Scenario: Create user in database (BAD)
Given I execute SQL "INSERT INTO users VALUES (1, 'john@example.com', 'password123')"
When I call POST /api/users with JSON {"id": 1, "email": "john@example.com"}
Then the HTTP response code should be 201
And the response body should contain "id"
# ✅ CORRECT: Business language
Scenario: Register new user (GOOD)
Given I am a new user with email "john@example.com"
When I register
Then my account should be created
And I should receive a welcome email
Anti-Patterns
❌ Skipping the Three Amigos
// WRONG - Developer writes scenarios in isolation
// - Misses business context and edge cases
// - Technical jargon creeps in
// - No shared understanding
// Result: Scenarios don't reflect actual requirements
// ✅ CORRECT
// - Schedule regular Three Amigos sessions
// - Invite BA, Tester, Developer
// - Use Example Mapping
// - Build shared understanding before coding
❌ Writing Scenarios After Implementation
// WRONG - Code-first, scenarios-later
// - Scenarios written to match code (confirmation bias)
// - Miss opportunities to improve design
// - No discovery value
// Result: Scenarios become glorified unit tests
// ✅ CORRECT
// - Write scenarios during discovery (before coding)
// - Let scenarios drive implementation
// - Use scenarios to guide TDD
// Result: Living specification that drives design
❌ Treating BDD as Just Another Testing Tool
// WRONG - "We do BDD because we use Cucumber"
// - Focus only on automation
// - Skip collaboration and discovery
// - No business stakeholder involvement
// Result: Technical tests in Gherkin format
// ✅ CORRECT
// - BDD is about collaboration first, automation second
// - Three Amigos sessions are essential
// - Business stakeholders review scenarios
// Result: Shared understanding and living docs
Testing Strategies
BDD Testing Pyramid
// Layer 1: Acceptance Tests (BDD scenarios)
// - Test business behavior end-to-end
// - Written in Gherkin (Given-When-Then)
// - Executable by business stakeholders
// - Slower, broader scope
@CucumberOptions(features = "src/test/resources/features")
public class AcceptanceTests {
// Cucumber scenarios testing complete features
}
// Layer 2: Integration Tests (traditional)
// - Test component interactions
// - Database, API, message queues
// - Faster than E2E, narrower scope
@SpringBootTest
public class DiscountServiceIntegrationTest {
@Test
void shouldApplyDiscountToOrder() { }
}
// Layer 3: Unit Tests (TDD)
// - Test individual components
// - Fast, isolated, focused
// - Support BDD scenarios
public class DiscountCalculatorTest {
@Test
void shouldCalculatePercentageDiscount() { }
}
// ✅ Strategy: Use BDD for acceptance criteria
// Use TDD for implementation details
Organizing BDD Tests
# Organize features by business capability, not technical layer
# ✅ GOOD structure
features/
├── order-management/
│ ├── create-order.feature
│ ├── cancel-order.feature
│ └── modify-order.feature
├── discount-codes/
│ ├── apply-discount.feature
│ └── manage-discounts.feature
└── customer-account/
├── register.feature
└── update-profile.feature
# ❌ BAD structure (by technical layer)
features/
├── api/
├── database/
└── ui/
Tagging Strategy
Feature: Apply Discount Codes
@smoke @critical
Scenario: Apply valid discount code
# Critical path, run in smoke tests
@regression @slow
Scenario: Verify all discount types
# Full regression, slower test
@wip @skip
Scenario: Apply discount with loyalty points
# Work in progress, skip in CI
@bug-1234 @regression
Scenario: Fix: Discount rounds to nearest cent
# Regression test for specific bug
# Run specific test suites
mvn test -Dcucumber.filter.tags="@smoke"
mvn test -Dcucumber.filter.tags="@regression and not @slow"
mvn test -Dcucumber.filter.tags="@critical or @smoke"
Living Documentation
// Generate readable reports from scenarios
@CucumberOptions(
plugin = {
"pretty", // Console output
"html:target/cucumber-reports/cucumber.html", // HTML report
"json:target/cucumber-reports/cucumber.json", // JSON for further processing
"junit:target/cucumber-reports/cucumber.xml" // JUnit XML for CI
}
)
// Integrate with documentation tools:
// - Cucumber-JVM reports → HTML documentation
// - Export to Confluence/Wiki
// - Generate PDF reports
// - Link scenarios to user stories in Jira
// Result: Always up-to-date documentation
References
- The Cucumber Book: Behaviour-Driven Development for Testers and Developers
- BDD in Action (John Ferguson Smart)
- Specification by Example (Gojko Adzic)
- Cucumber Documentation
- Example Mapping