Last updated: 2026-09-26
TDD & BDD
TDD is not about testing. It's about design." — Kent Beck [^beck2002]
The Core Cycles Applied / MethodologicalKnowledge with a 5–10 year half-life — stable practice
TDD — Red → Green → Refactor
BDD — Given → When → Then
TDD vs BDD — Complementary, Not Competing Applied / MethodologicalKnowledge with a 5–10 year half-life — stable practice
| 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 |
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 Applied / MethodologicalKnowledge with a 5–10 year half-life — stable practice
- 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
BDD — Specification by Example Applied / MethodologicalKnowledge with a 5–10 year half-life — stable practice
The Three Amigos
| Role | Contribution |
|---|---|
| Product | Business rules, acceptance criteria |
| Development | Technical feasibility, edge cases |
| QA/Testing | Edge cases, negative scenarios, automation |
Living Documentation
gherkin (executable spec)
│
▼
Cucumber/SpecFlow runs it
│
▼
Each step → Glue code → TDD implementation
│
▼
HTML report = Living Documentation
│
▼
Always in sync with code!
Gherkin Best Practices
| Anti-pattern | Better |
|---|---|
Given I am on the login page |
Given I have an account |
When I click the blue button |
When I submit my credentials |
Then I should see success message |
Then I am logged in |
| UI-coupled steps | Business-intent steps |
| Imperative (how) | Declarative (what) |
TDD/BDD Tooling Ecosystem Ephemeral / ToolingKnowledge that evolves in months to a year — check for updates
| Language | TDD Framework | BDD Framework | Notes |
|---|---|---|---|
| Python | pytest, unittest | behave, pytest-bdd | pytest-bdd = pytest + Gherkin |
| JavaScript/TypeScript | Vitest, Jest | Cucumber.js, Playwright BDD | Playwright has built-in BDD |
| Java | JUnit 5, TestNG | Cucumber JVM, JGiven | JGiven = code-based BDD |
| C#/.NET | xUnit, NUnit | SpecFlow, BDDfy | SpecFlow = mature |
| Go | testing, testify | Godog, Ginkgo | Godog = Cucumber for Go |
| Rust | built-in test | cucumber-rust | Growing ecosystem |
Example: From BDD Scenario to TDD Implementation Applied / MethodologicalKnowledge with a 5–10 year half-life — stable practice
1. Feature File (Business)
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 a business stakeholder who's never seen either codebase:Gherkin is the syntax. Cucumber and friends run it
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.cpp
#include <string>
#include <cassert>
class Customer { /* ... */ };
class Cart { /* ... */ };
class Money { /* ... */ };
class PricingEngine { /* ... */ };
void customer_step(const std::string& tier, int years) {
return Customer(tier, years);
}
void cart_step(int amount) {
return Cart(Money(amount));
}
Money calculate_discount_step(const Customer& customer, const Cart& cart) {
return PricingEngine().calculate_discount(customer, cart);
}
void verify_discount_step(const Money& discount, int 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
3. TDD Implementation (Driven by failing step)
# tests/unit/pricing/test_engine.py
def test_gold_customer_3_years_gets_15_percent():
customer = Customer(tier="gold", years_active=3)
cart = Cart(Money(1000))
discount = PricingEngine().calculate_discount(customer, cart)
assert discount == Money(150)
# Implementation evolves:
class PricingEngine:
def calculate_discount(self, customer, cart):
base = self._base_discount(customer.tier, cart.total)
loyalty = self._loyalty_bonus(customer.years_active)
return base + loyalty
// tests/unit/pricing/test_engine.cpp
#include <cassert>
void test_gold_customer_3_years_gets_15_percent() {
Customer customer("gold", 3);
Cart cart(Money(1000));
Money discount = PricingEngine().calculate_discount(customer, cart);
assert(discount == Money(150));
}
class PricingEngine {
public:
Money calculate_discount(const Customer& customer, const Cart& cart) {
Money base = _base_discount(customer.tier, cart.total());
Money loyalty = _loyalty_bonus(customer.years_active);
return base + loyalty;
}
private:
Money _base_discount(const std::string& tier, const Money& total);
Money _loyalty_bonus(int years_active);
};
// 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 Applied / MethodologicalKnowledge with a 5–10 year half-life — stable practice
| 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 Ephemeral / ToolingKnowledge that evolves in months to a year — check for updates
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
// PITest for Java
<plugin>
<groupId>org.pitest</groupId>
<artifactId>pitest-maven</artifactId>
<version>1.15.0</version>
<configuration>
<targetClasses>
<param>com.myapp.*</param>
</targetClasses>
<mutationThreshold>80</mutationThreshold>
</configuration>
</plugin>
// Stryker.NET for C#
// dotnet tool install -g dotnet-stryker
// dotnet stryker --threshold-break 80
# mutant for Ruby
# gem install mutant mutant-rspec
# bundle exec mutant run --integration rspec 'PricingEngine*'
# In CI:
# - bundle exec mutant run
# - mutant reports killed vs alive mutations
# - Threshold: >80% mutation coverage
Contract Testing (Pact)
# Consumer-driven contract testing with Pact
from pact import Consumer, Provider, Given, UponReceiving, WithState
pact = Consumer('OrderService').has_pact_with(Provider('PaymentService'))
(pact
.given('a valid payment request')
.upon_receiving('a charge request')
.with_request('POST', '/payment/charge', body={
'amount_cents': 1999,
'currency': 'USD',
'source': 'tok_visa'
})
.will_respond_with(200, body={
'transaction_id': 'txn_123',
'status': 'succeeded'
}))
pact.verify()
// 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
// Pact JVM
@ExtendWith(PactConsumerTestExt.class)
@PactTestFor(providerName = "PaymentService")
class PaymentContractTest {
@Test
@Pact(consumer = "OrderService", provider = "PaymentService")
V2RequestResponsePact createPact(PactDslWithProvider builder) {
return builder
.given("a valid payment request")
.uponReceiving("a charge request")
.path("/payment/charge")
.method("POST")
.body(newJsonBody(o -> o
.numberType("amount_cents", 1999)
.stringType("currency", "USD")
.stringType("source", "tok_visa")
))
.willRespondWith()
.status(200)
.body(newJsonBody(o -> o
.stringType("transaction_id", "txn_123")
.stringType("status", "succeeded")
))
.toPact(V2Pact.class);
}
}
// 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" })
Related Topics
- BDD as Specification: A Case Study in Deriving Code from Scenarios — a real system where a BDD scenario is used as an actual implementation-deriving specification, not just executable documentation.
- Mutation Testing — the check on whether the tests this page's red-green-refactor cycle produces would actually catch a real bug.
- Testing Fundamentals — the wider testing-pyramid context TDD/BDD's unit-to-acceptance layers sit inside.
- Cross-Pollination of Ideas — domain-driven design's ubiquitous language, one of the two ideas BDD grew out of.
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 Applied / MethodologicalKnowledge with a 5–10 year half-life — stable practice
| 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 |
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.
See BDD as Specification for a 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. For red-green-refactor and Given/When/Then applied to a single learner deciding when a study topic is actually finished, rather than to production code, see A Definition of Done for Studying.
Graduate Attributes
Modelled on this page
- Confident Communicator: Technical concepts are explained using a language-agnostic format designed to be understood by both developers and business stakeholders.
- Collaborative: The role of the Three Amigos is presented as a mechanism for integrating different viewpoints during specification.
- Digitally Fluent: Specific digital tools and ecosystems for testing are catalogued alongside their practical applications.
University of Reading Graduate Attributes framework.