TDDTDD 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 behaviour]
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 behaviour
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
KeyKey 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());
}
# 1. RED — Write failing test (RSpec)
RSpec.describe Cart do
it "has zero total when empty" do
expect(Cart.new.total).to eq(Money.zero)
end
end
# 2. GREEN — Minimal implementation
class Cart
def total
Money.zero
end
end
# 3. REFACTOR — Clean up (none needed yet)
# Repeat for: add_item, remove_item, quantity, discounts...
# Real example after several cycles:
class Cart
def initialize
@items = []
end
def add(product, qty = 1)
@items << LineItem.new(product, qty)
end
def total
@items.sum(Money.zero, &:subtotal)
end
end
AA Gherkin feature file is deliberately language-agnostic — that's the whole point of the format, so the same scenario reads identically to a Python team, a Java team, or a business stakeholder who's never seen either codebase:
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);
}
}
# features/step_definitions/discount_steps.rb (Cucumber)
Given(/^a customer with "([^"]*)" tier and (\d+) years active$/) do |tier, years|
@customer = Customer.new(tier: tier, years_active: years.to_i)
end
Given(/^a cart with total \$(\d+)$/) do |amount|
@cart = Cart.new(Money.new(amount.to_i))
end
When("the discount is calculated") do
@discount = PricingEngine.new.calculate_discount(@customer, @cart)
end
Then(/^the discount should be \$(\d+)$/) do |amount|
expect(@discount).to eq(Money.new(amount.to_i))
end
// 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;
}
}
# spec/pricing/engine_spec.rb
RSpec.describe PricingEngine do
it "gives gold customers of 3 years a 15% discount" do
customer = Customer.new(tier: "gold", years_active: 3)
cart = Cart.new(Money.new(1000))
discount = described_class.new.calculate_discount(customer, cart)
expect(discount).to eq(Money.new(150))
end
end
# Implementation evolves:
class PricingEngine
def calculate_discount(customer, cart)
base = base_discount(customer.tier, cart.total)
loyalty = loyalty_bonus(customer.years_active)
base + loyalty
end
end
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);
}
}
# property-based testing with the prop_check gem
require "prop_check"
G = PropCheck::Generators
PropCheck.forall(G.array(G.integer)) do |xs|
raise "sort is not idempotent" unless xs.sort.sort == xs.sort
end
PropCheck.forall(G.string) do |s|
raise "reverse is not an involution" unless s.reverse.reverse == s
end
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"
});
}
}
# Consumer-driven contract testing with Pact
require "pact/consumer/rspec"
Pact.service_consumer "OrderService" do
has_pact_with "PaymentService" do
mock_service :payment_service do
port 1234
end
end
end
payment_service
.given("a valid payment request")
.upon_receiving("a charge request")
.with(method: :post, path: "/payment/charge",
body: { amount_cents: 1999, currency: "USD", source: "tok_visa" })
.will_respond_with(status: 200,
body: { 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. Practising
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
RememberRemember: 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.
See BDD as Specification for a real, working case study taking this page's "living documentation" claim one step further — a system that derives an actual rule from Given/When/Then scenarios, and turns an unsatisfiable scenario into a specific, answerable question rather than a bare test failure. TDD itself originated as one of the core engineering practices of Extreme Programming — see Extreme Programming for where it sits alongside pair programming and continuous integration, and Scrum for the process layer that (deliberately) says nothing about engineering practice and leaves this gap for XP to fill.
📝 Executive Bite-Sized Summary (Max 2 Short Sentences per Section)
Overview
TDD T DD is not about testing.
It's about design." — Kent Beck [^beck2002]
TDD vs BDD — Complementary, Not Competing
You don't choose one — you do both at different levels.
Key K ey insight : BDD scenarios become the acceptance criteria that drive TDD cycles.
1. Feature File (Business)
A A Gherkin feature file is deliberately language-agnostic — that's the whole point of the format, so the same scenario reads identically to a Python team, a Java team, or ...
Summary: TDD/BDD Maturity
BDD is a communication practice, not a testing tool.
Remember R emember : TDD is a design practice, not a testing practice.
🗺️ Site-Wide Concept Atlas
Cartographic Topological Map
🗺️ Pathway Interrogation: Hover over any terrain road line or city node
🌉 Bridge Concepts: Interrogating intermediate weight vectors across the atlas
📍 Explore Map: Click any node circle or road line to navigate
🧭 You Are Here
✨ Active Concept Hit
⛰️ Mountain Ridge
📤 Connection Road
🐉 "Here Be Dragons"
Structured taxonomy index of site concepts and articles mapped by SOM topological affinity. Keyboard & screen-reader accessible.