# Given-When-Then (BDD Scenario Structure)

## Problem

Tests written in pure code can be opaque to non-developers. Product owners, QA engineers, and business analysts struggle to understand what behaviour is being verified. This disconnect leads to specifications that diverge from tests and requirements that go unverified.

## Solution

Structure tests using the **Given-When-Then** format from Behaviour-Driven Development (BDD):

1. **Given** — Establish the initial context (preconditions).
2. **When** — The action or event occurs.
3. **Then** — The expected observable outcome.

This is semantically equivalent to Arrange-Act-Assert but uses language that maps directly to user stories and acceptance criteria.

## When to Use

- Acceptance tests that need to be readable by non-developers.
- BDD workflows with tools like Behave (Python), Cucumber, or Jest-Cucumber.
- Documenting expected behaviour as living specifications.
- When stakeholders participate in defining test scenarios.

## When to Avoid

- Low-level unit tests where AAA is more natural and concise.
- Performance or load tests where scenario structure adds no value.
- When the team has no BDD workflow and the overhead isn't justified.

## Pseudocode

```
Scenario: Applying a percentage discount to a cart

  Given a shopping cart with items totalling $100
    And a 20% discount coupon "SAVE20"

  When the customer applies the coupon

  Then the cart total should be $80
    And the applied coupon should be "SAVE20"
```

## Python

```python
"""Given-When-Then BDD scenario structure in Python."""
from dataclasses import dataclass, field


# ── Production code ────────────────────────────────────────────

@dataclass
class CartItem:
    name: str
    price: float

@dataclass
class ShoppingCart:
    items: list[CartItem] = field(default_factory=list)
    applied_coupon: str | None = None
    discount_pct: float = 0.0

    def add(self, name: str, price: float) -> None:
        self.items.append(CartItem(name=name, price=price))

    def apply_coupon(self, code: str, discount_pct: float) -> None:
        if self.applied_coupon:
            raise ValueError("A coupon is already applied")
        self.applied_coupon = code
        self.discount_pct = discount_pct

    @property
    def subtotal(self) -> float:
        return sum(item.price for item in self.items)

    @property
    def total(self) -> float:
        return self.subtotal * (1 - self.discount_pct / 100)


# ── BDD-style tests ───────────────────────────────────────────

def test_apply_percentage_discount():
    print("Scenario: Applying a percentage discount to a cart")

    # Given a shopping cart with items totalling $100
    cart = ShoppingCart()
    cart.add("Widget", 60.0)
    cart.add("Gadget", 40.0)
    print(f"  Given a cart with subtotal ${cart.subtotal:.0f}")

    # And a 20% discount coupon "SAVE20"
    coupon_code = "SAVE20"
    coupon_discount = 20.0
    print(f"  And a {coupon_discount:.0f}% discount coupon \"{coupon_code}\"")

    # When the customer applies the coupon
    cart.apply_coupon(coupon_code, coupon_discount)
    print(f"  When the customer applies the coupon")

    # Then the cart total should be $80
    assert cart.total == 80.0, f"Expected 80, got {cart.total}"
    print(f"  Then the cart total should be ${cart.total:.0f} -> PASS")

    # And the applied coupon should be "SAVE20"
    assert cart.applied_coupon == "SAVE20"
    print(f"  And the applied coupon is \"{cart.applied_coupon}\" -> PASS")


def test_cannot_apply_two_coupons():
    print("\nScenario: Cannot apply two coupons")

    # Given a cart with a coupon already applied
    cart = ShoppingCart()
    cart.add("Item", 50.0)
    cart.apply_coupon("FIRST10", 10.0)
    print(f"  Given a cart with coupon \"FIRST10\" applied")

    # When the customer tries to apply another coupon
    print(f"  When the customer applies another coupon")
    try:
        cart.apply_coupon("SECOND20", 20.0)
        assert False, "Expected ValueError"
    except ValueError as e:
        # Then an error is raised
        assert "already applied" in str(e)
        print(f"  Then an error is raised: \"{e}\" -> PASS")


def test_empty_cart_total_is_zero():
    print("\nScenario: Empty cart has zero total")

    # Given an empty shopping cart
    cart = ShoppingCart()
    print("  Given an empty shopping cart")

    # When we check the total
    total = cart.total
    print("  When we check the total")

    # Then the total is 0
    assert total == 0.0
    print(f"  Then the total is ${total:.0f} -> PASS")


if __name__ == "__main__":
    test_apply_percentage_discount()
    test_cannot_apply_two_coupons()
    test_empty_cart_total_is_zero()
    print("\nAll Given-When-Then scenarios passed.")
```

## JavaScript

```javascript
// given_when_then.js — BDD scenario structure in JavaScript

// ── Production code ──────────────────────────────────────────

class ShoppingCart {
  constructor() {
    this.items = [];
    this.appliedCoupon = null;
    this.discountPct = 0;
  }

  add(name, price) {
    this.items.push({ name, price });
  }

  applyCoupon(code, discountPct) {
    if (this.appliedCoupon) throw new Error("A coupon is already applied");
    this.appliedCoupon = code;
    this.discountPct = discountPct;
  }

  get subtotal() {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }

  get total() {
    return this.subtotal * (1 - this.discountPct / 100);
  }
}

// ── BDD-style tests ──────────────────────────────────────────

function testApplyPercentageDiscount() {
  console.log("Scenario: Applying a percentage discount to a cart");

  // Given a shopping cart with items totalling $100
  const cart = new ShoppingCart();
  cart.add("Widget", 60);
  cart.add("Gadget", 40);
  console.log(`  Given a cart with subtotal $${cart.subtotal}`);

  // And a 20% discount coupon "SAVE20"
  const couponCode = "SAVE20";
  const couponDiscount = 20;
  console.log(`  And a ${couponDiscount}% discount coupon "${couponCode}"`);

  // When the customer applies the coupon
  cart.applyCoupon(couponCode, couponDiscount);
  console.log("  When the customer applies the coupon");

  // Then the cart total should be $80
  console.assert(cart.total === 80, `Expected 80, got ${cart.total}`);
  console.log(`  Then the cart total should be $${cart.total} -> PASS`);

  // And the applied coupon should be "SAVE20"
  console.assert(cart.appliedCoupon === "SAVE20");
  console.log(`  And the applied coupon is "${cart.appliedCoupon}" -> PASS`);
}

function testCannotApplyTwoCoupons() {
  console.log("\nScenario: Cannot apply two coupons");

  // Given a cart with a coupon already applied
  const cart = new ShoppingCart();
  cart.add("Item", 50);
  cart.applyCoupon("FIRST10", 10);
  console.log('  Given a cart with coupon "FIRST10" applied');

  // When the customer tries to apply another coupon
  console.log("  When the customer applies another coupon");
  try {
    cart.applyCoupon("SECOND20", 20);
    throw new Error("Expected error");
  } catch (e) {
    // Then an error is raised
    console.assert(e.message.includes("already applied"));
    console.log(`  Then an error is raised: "${e.message}" -> PASS`);
  }
}

function testEmptyCartTotalIsZero() {
  console.log("\nScenario: Empty cart has zero total");

  // Given an empty shopping cart
  const cart = new ShoppingCart();
  console.log("  Given an empty shopping cart");

  // When we check the total
  const total = cart.total;
  console.log("  When we check the total");

  // Then the total is 0
  console.assert(total === 0);
  console.log(`  Then the total is $${total} -> PASS`);
}

testApplyPercentageDiscount();
testCannotApplyTwoCoupons();
testEmptyCartTotalIsZero();
console.log("\nAll Given-When-Then scenarios passed.");
```

## Related Patterns

- **Arrange-Act-Assert** — AAA is the developer-centric equivalent of Given-When-Then; same structure, different vocabulary.
- **Specification** — BDD scenarios can be linked to formal specifications, making acceptance criteria executable.
