"TDD is not about testing. It's about design." โ Kent Beck [^beck2002]
The Core Cycles
TDD โ Red โ Green โ Refactor
graph LR
RED[RED: Write failing test] --> GREEN[GREEN: Make it pass with minimal code]
GREEN --> REFACTOR[REFACTOR: Clean up without changing behavior]
REFACTOR --> RED
BDD โ Given โ When โ Then
graph LR
GIVEN[GIVEN: Context] --> WHEN[WHEN: Action]
WHEN --> THEN[THEN: Outcome]
TDD vs BDD โ Complementary, Not Competing
Dimension
TDD
BDD
Relationship
Focus
Developer-facing design
Business-facing behavior
BDD = TDD at higher level
Language
Code (unit test syntax)
Natural language (Gherkin)
BDD specs โ TDD tests
Audience
Developers
Devs + QA + Product + Stakeholders
Shared vocabulary
Granularity
Function/method
User journey / feature
BDD decomposes to TDD
Tooling
xUnit frameworks
Cucumber, SpecFlow, Behave
BDD tools drive TDD
Key insight: BDD scenarios become the acceptance criteria that drive TDD cycles. You don't choose one โ you do both at different levels.
TDD in Practice โ The Three Laws
You may not write production code until you have written a failing unit test.
You may not write more of a unit test than is sufficient to fail (and not compile is failing).
You may not write more production code than is sufficient to pass the currently failing test.
Example: Implementing a Shopping Cart
# 1. RED โ Write failing test
def test_empty_cart_has_zero_total():
cart = Cart()
assert cart.total() == Money.zero()
# 2. GREEN โ Minimal implementation
class Cart:
def total(self):
return Money.zero()
# 3. REFACTOR โ Clean up (none needed yet)
# Repeat for: add_item, remove_item, quantity, discounts...
# Real example after several cycles:
class Cart:
def __init__(self):
self._items = []
def add(self, product: Product, qty: int = 1):
self._items.append(LineItem(product, qty))
def total(self) -> Money:
return sum(item.subtotal() for item in self._items)
#include <memory>
#include <vector>
class Money {
public:
explicit Money(long cents = 0) : cents_(cents) {}
static Money zero() { return Money(0); }
// operators...
private:
long cents_;
};
class Product { /* ... */ };
class LineItem { /* ... */ };
// 1. RED โ Write failing test
void test_empty_cart_has_zero_total() {
Cart cart;
assert(cart.total() == Money::zero());
}
// 2. GREEN โ Minimal implementation
class Cart {
public:
Money total() const { return Money::zero(); }
};
// 3. REFACTOR โ Clean up (none needed yet)
// Repeat for: add_item, remove_item, quantity, discounts...
// Real example after several cycles:
class Cart {
std::vector<LineItem> items_;
public:
void add(const Product& product, int qty = 1) {
items_.emplace_back(product, qty);
}
Money total() const {
Money total = Money::zero();
for (const auto& item : items_)
total += item.subtotal();
return total;
}
private:
std::vector<LineItem> items_;
};
import java.util.*;
public final class Money {
private final long cents;
public Money(long cents) { this.cents = cents; }
public static Money zero() { return new Money(0); }
// operators...
}
public final class Product { /* ... */ }
public final class LineItem { /* ... */ }
// 1. RED โ Write failing test
@Test
void testEmptyCartHasZeroTotal() {
Cart cart = new Cart();
assertEquals(Money.zero(), cart.total());
}
// 2. GREEN โ Minimal implementation
public final class Cart {
public Money total() { return Money.zero(); }
}
// 3. REFACTOR โ Clean up (none needed yet)
// Repeat for: add_item, remove_item, quantity, discounts...
// Real example after several cycles:
public final class Cart {
private final List<LineItem> items = new ArrayList<>();
public void add(Product product, int qty) {
items.add(new LineItem(product, qty));
}
public Money total() {
Money total = Money.zero();
for (LineItem item : items) {
total = total.add(item.subtotal());
}
return total;
}
}
using System;
using System.Collections.Generic;
public readonly record struct Money(long Cents) {
public static Money Zero => new Money(0);
public static Money operator +(Money a, Money b) => new Money(a.Cents + b.Cents);
}
// 1. RED โ Write failing test
[Fact]
public void TestEmptyCartHasZeroTotal() {
var cart = new Cart();
Assert.Equal(Money.Zero, cart.Total());
}
// 2. GREEN โ Minimal implementation
public sealed class Cart {
public Money Total() => Money.Zero;
}
// 3. REFACTOR โ Clean up (none needed yet)
// Repeat for: add_item, remove_item, quantity, discounts...
// Real example after several cycles:
public sealed class Cart {
private readonly List<LineItem> _items = new();
public void Add(Product product, int qty = 1) =>
_items.Add(new LineItem(product, qty));
public Money Total() => _items.Aggregate(Money.Zero, (sum, item) => sum + item.Subtotal());
}
Feature: Discount Calculation
Scenario: Gold tier customer gets loyalty discount
Given a customer with "gold" tier and 3 years active
And a cart with total $1000
When the discount is calculated
Then the discount should be $150
Feature: Discount Calculation
Scenario: Gold tier customer gets loyalty discount
Given a customer with "gold" tier and 3 years active
And a cart with total $1000
When the discount is calculated
Then the discount should be $150
Feature: Discount Calculation
Scenario: Gold tier customer gets loyalty discount
Given a customer with "gold" tier and 3 years active
And a cart with total $1000
When the discount is calculated
Then the discount should be $150
Feature: Discount Calculation
Scenario: Gold tier customer gets loyalty discount
Given a customer with "gold" tier and 3 years active
And a cart with total $1000
When the discount is calculated
Then the discount should be $150
2. Step Definitions (Glue)
# tests/steps/discount_steps.py
from pytest_bdd import given, when, then, parsers
@given(parsers.parse('a customer with "{tier}" tier and {years:d} years active'))
def customer(tier, years):
return Customer(tier=tier, years_active=years)
@given(parsers.parse('a cart with total ${amount:d}'))
def cart(amount):
return Cart(Money(amount))
@when('the discount is calculated')
def calculate_discount(customer, cart):
return PricingEngine().calculate_discount(customer, cart)
@then(parsers.parse('the discount should be ${amount:d}'))
def verify_discount(discount, amount):
assert discount == Money(amount)
// tests/steps/discount_steps.java
import io.cucumber.java.en.*;
public class DiscountSteps {
private Customer customer;
private Cart cart;
private Money discount;
@Given("a customer with {string} tier and {int} years active")
public void customer(String tier, int years) {
customer = new Customer(tier, years);
}
@Given("a cart with total ${int}")
public void cart(int amount) {
cart = new Cart(new Money(amount));
}
@When("the discount is calculated")
public void calculate_discount() {
discount = new PricingEngine().calculate_discount(customer, cart);
}
@Then("the discount should be ${int}")
public void verify_discount(int amount) {
assert discount.equals(new Money(amount));
}
}
// tests/steps/discount_steps.cs
using TechTalk.SpecFlow;
[Binding]
public class DiscountSteps {
private Customer _customer;
private Cart _cart;
private Money _discount;
[Given(@"a customer with ""(.*)"" tier and (\d+) years active")]
public void Customer(string tier, int years) {
_customer = new Customer(tier, years);
}
[Given(@"a cart with total \$(.*)")]
public void Cart(int amount) {
_cart = new Cart(new Money(amount));
}
[When(@"the discount is calculated")]
public void CalculateDiscount() {
_discount = new PricingEngine().CalculateDiscount(_customer, _cart);
}
[Then(@"the discount should be \$(.*)")]
public void VerifyDiscount(int amount) {
Assert.AreEqual(new Money(amount), _discount);
}
}
// tests/unit/pricing/test_engine.java
import static org.junit.jupiter.api.Assertions.*;
class PricingEngineTest {
@Test
void testGoldCustomer3YearsGets15Percent() {
Customer customer = new Customer("gold", 3);
Cart cart = new Cart(new Money(1000));
Money discount = new PricingEngine().calculate_discount(customer, cart);
assertEquals(new Money(150), discount);
}
}
public class PricingEngine {
public Money calculate_discount(Customer customer, Cart cart) {
Money base = _base_discount(customer.tier, cart.total());
Money loyalty = _loyalty_bonus(customer.years_active);
return base.add(loyalty);
}
}
// tests/unit/pricing/test_engine.cs
using Xunit;
public class PricingEngineTests {
[Fact]
public void TestGoldCustomer3YearsGets15Percent() {
var customer = new Customer("gold", 3);
var cart = new Cart(new Money(1000));
var discount = new PricingEngine().CalculateDiscount(customer, cart);
Assert.Equal(new Money(150), discount);
}
}
public class PricingEngine {
public Money CalculateDiscount(Customer customer, Cart cart) {
var base = _base_discount(customer.Tier, cart.Total);
var loyalty = _loyalty_bonus(customer.YearsActive);
return base + loyalty;
}
}
Common TDD/BDD Pitfalls
Pitfall
Symptom
Fix
Testing private methods
Tests break on refactor
Test public API; extract if needed
Over-mocking
Brittle tests, false confidence
Mock boundaries, not internals
BDD = UI automation
Slow, flaky suites
BDD = business rules, not UI scripts
No refactoring step
Accumulated technical debt
Make refactoring a non-negotiable step
Tests as afterthought
Tests don't drive design
Write test first, always
Advanced Topics
Property-Based Testing
# property-based testing with hypothesis
from hypothesis import given, strategies as st
@given(st.lists(st.integers()))
def test_sort_idempotent(xs):
assert sorted(sorted(xs)) == sorted(xs)
@given(st.text())
def test_reverse_involution(s):
assert s[::-1][::-1] == s
// RapidCheck for C++
#include <rapidcheck/rapidcheck.h>
void test_sort_idempotent() {
rc::check("sort is idempotent", [](const std::vector<int>& xs) {
auto sorted_once = xs;
std::sort(sorted_once.begin(), sorted_once.end());
auto sorted_twice = sorted_once;
std::sort(sorted_twice.begin(), sorted_twice.end());
RC_ASSERT(sorted_once == sorted_twice);
});
}
// jqwik for Java property-based testing
import net.jqwik.api.*;
class PropertyTests {
@Property
boolean sortIsIdempotent(@ForAll List<Integer> xs) {
var sortedOnce = new ArrayList<>(xs);
Collections.sort(sortedOnce);
var sortedTwice = new ArrayList<>(sortedOnce);
Collections.sort(sortedTwice);
return sortedOnce.equals(sortedTwice);
}
}
// FsCheck for C#
using FsCheck;
using FsCheck.Xunit;
public class PropertyTests {
[Property]
public bool SortIsIdempotent(List<int> xs) {
var sortedOnce = xs.OrderBy(x => x).ToList();
var sortedTwice = sortedOnce.OrderBy(x => x).ToList();
return sortedOnce.SequenceEqual(sortedTwice);
}
}
Mutation Testing
# mutmut for Python
# pip install mutmut
# mutmut run --paths-to-mutate=src/
# In CI:
# - mutmut run
# - mutmut results
# - Threshold: >80% mutation score
// Mull for C++
// clang -fprofile-instr-generate -fcoverage-mapping
// mull-cxx --thread-count=4 --mutation-score-threshold=80
// Pact C++ is limited
// Use pact-verifier CLI for verification
# pact-verifier --provider-base-url=http://localhost:8080 \
# --pact-url=./pacts/order_service-payment_service.json
// PactNet
using PactNet;
using Xunit;
public class PaymentContractTests {
[Fact]
public void PaymentContract() {
var config = new PactConfig { PactDir = "./pacts" };
var pact = new PactVerifier(new PactConfig { PactDir = "./pacts" });
var builder = new V2PactBuilder("OrderService", "PaymentService");
builder
.Given("a valid payment request")
.UponReceiving("a charge request")
.With(new ProviderServiceRequest {
Method = HttpVerb.Post,
Path = "/payment/charge",
Body = new { amount_cents = 1999, currency = "USD", source = "tok_visa" }
})
.WillRespondWith(200, new {
transaction_id = "txn_123",
status = "succeeded"
});
}
}
Resources
Books
Test Driven Development: By Example โ Kent Beck
The Cucumber Book โ Matt Wynne & Aslak Hellesรธy
Specification by Example โ Gojko Adzic
Growing Object-Oriented Software, Guided by Tests โ Steve Freeman & Nat Pryce
TDD for Embedded C โ James W. Grenning
Tools
Language
TDD
BDD
Mutation
Contract
Python
pytest
pytest-bdd, behave
mutmut
pact-python
C++
Catch2, GoogleTest
Cucumber-cpp
Mull
Pact-cpp
Java
JUnit 5
Cucumber JVM, JGiven
PITest
Pact JVM
C#
xUnit, NUnit
SpecFlow, BDDfy
Stryker.NET
PactNet
Go
testing
Godog, Ginkgo
Go-mutesting
Pact-go
Talks
"TDD: Test Desired Design" by Ian Cooper โ NDC
"BDD: Beyond the Basics" by Gojko Adzic โ NDC
"Mutation Testing: The What, Why, and How" by Greg L. Turnquist โ Spring I/O
"Pact: Consumer Driven Contracts" by Beth Skurrie โ รredev
Summary: TDD/BDD Maturity
Level
TDD
BDD
Tooling
Culture
1. Initial
Ad-hoc tests
None
Manual
Testing after coding
2. Emerging
Unit tests written
Basic scenarios
Manual runs
Tests as afterthought
3. Practicing
RED-GREEN-REFACTOR
Gherkin + Glue
CI integration
TDD/BDD routine
4. Proficient
Property-based + Mutation
Specification by Example
CI gates + gates
Tests drive design
5. Mastery
Mutation score >80%
Living docs + contracts
Zero-touch CI/CD
Quality culture
Remember: TDD is a design practice, not a testing practice. BDD is a communication practice, not a testing tool. Together, they ensure you build the right thing and build the thing right.