Skip to content
Home / Skills / Bdd / Behavior-Driven Development
BD

Behavior-Driven Development

Bdd principles v1.0.0

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

StepActivityOutputParticipants
1. Story CardReview user storyUnderstanding of goalAll three amigos
2. RulesDefine business rulesYellow cards (rules)BA leads, all contribute
3. ExamplesCreate concrete examplesBlue cards (examples)Tester leads, all contribute
4. QuestionsIdentify unknownsRed cards (questions)All identify gaps
5. ScenariosConvert to GherkinExecutable specsDeveloper implements

BDD vs TDD

AspectTDDBDD
FocusInternal qualityExternal behavior
LanguageTechnical (assertions)Natural (Given-When-Then)
AudienceDevelopersEntire team
ScopeUnit testsAcceptance tests
WhenImplementation phaseDiscovery & implementation
ArtifactTest codeLiving 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