tdd-workflow.md 5.2 KB

Test-Driven Development (TDD)

Red-Green-Refactor cycle for quality code.

The TDD Cycle

┌─────────────────────────────────────────┐
│                                         │
│   ┌─────┐    ┌─────┐    ┌─────────┐    │
│   │ RED │ -> │GREEN│ -> │REFACTOR │ ──┐│
│   └─────┘    └─────┘    └─────────┘   ││
│       ^                               │ │
│       └───────────────────────────────┘ │
│                                         │
└─────────────────────────────────────────┘

1. RED: Write a Failing Test

# Start with the test
def test_calculate_discount_applies_percentage():
    cart = Cart()
    cart.add_item(Item(price=100))

    total = cart.calculate_total(discount_percent=10)

    assert total == 90  # Fails - function doesn't exist yet

2. GREEN: Make It Pass (Minimal Code)

# Write minimal code to pass
class Cart:
    def __init__(self):
        self.items = []

    def add_item(self, item):
        self.items.append(item)

    def calculate_total(self, discount_percent=0):
        total = sum(item.price for item in self.items)
        return total * (1 - discount_percent / 100)

3. REFACTOR: Improve the Code

# Clean up while tests pass
class Cart:
    def __init__(self):
        self._items: list[Item] = []

    def add_item(self, item: Item) -> None:
        self._items.append(item)

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

    def calculate_total(self, discount_percent: int = 0) -> Decimal:
        discount_multiplier = Decimal(100 - discount_percent) / 100
        return self.subtotal * discount_multiplier

TDD Rules

Three Laws of TDD

  1. Don't write production code until you have a failing test
  2. Write only enough test to fail (compilation counts)
  3. Write only enough production code to pass the test

Test Size Rules

Tests should be:
- Fast (< 100ms each)
- Isolated (no shared state)
- Repeatable (same result every time)
- Self-validating (pass/fail, no manual inspection)
- Timely (written before production code)

TDD Workflow Example

Step 1: List Test Cases

Feature: Shopping Cart Discount

Test cases:
[ ] Empty cart returns 0
[ ] Single item returns item price
[ ] Multiple items returns sum
[ ] Percentage discount applied correctly
[ ] Maximum discount capped at 50%
[ ] Negative discount treated as 0

Step 2: Start with Simplest Test

def test_empty_cart_returns_zero():
    cart = Cart()
    assert cart.calculate_total() == 0

Step 3: Implement and Move to Next

# After passing, add next test
def test_single_item_returns_price():
    cart = Cart()
    cart.add_item(Item(price=50))
    assert cart.calculate_total() == 50

Step 4: Build Up Complexity

def test_discount_capped_at_50_percent():
    cart = Cart()
    cart.add_item(Item(price=100))

    total = cart.calculate_total(discount_percent=75)

    assert total == 50  # Capped at 50% max discount

When to Use TDD

Good For:

  • Business logic
  • Algorithms
  • Data transformations
  • API contracts
  • Complex conditionals

Less Suitable For:

  • UI/visual elements
  • Exploratory prototyping
  • One-off scripts
  • Integration with external systems

TDD Anti-Patterns

Testing Implementation Details

# BAD - Tests internal state
def test_cart_has_items_list():
    cart = Cart()
    cart.add_item(Item(price=10))
    assert len(cart._items) == 1  # Tests implementation!

# GOOD - Tests behavior
def test_cart_counts_items():
    cart = Cart()
    cart.add_item(Item(price=10))
    assert cart.item_count == 1  # Tests public interface

Tests That Mirror Code

# BAD - Test duplicates implementation
def test_calculate_total():
    cart = Cart()
    cart.add_item(Item(price=10))
    cart.add_item(Item(price=20))

    # This is just reimplementing the function
    expected = 10 + 20
    assert cart.calculate_total() == expected

# GOOD - Tests expected outcome
def test_calculate_total():
    cart = Cart()
    cart.add_item(Item(price=10))
    cart.add_item(Item(price=20))

    assert cart.calculate_total() == 30

Kata Practice

String Calculator

Create a calculator that takes a string of numbers and returns their sum.

Step 1: "" returns 0
Step 2: "1" returns 1
Step 3: "1,2" returns 3
Step 4: Handle unknown number of numbers
Step 5: Handle newlines as delimiters: "1\n2,3" returns 6
Step 6: Support custom delimiters: "//;\n1;2" returns 3
Step 7: Throw on negative numbers with message including all negatives

FizzBuzz

Step 1: Return "1" for 1
Step 2: Return "2" for 2
Step 3: Return "Fizz" for 3
Step 4: Return "Buzz" for 5
Step 5: Return "Fizz" for 6 (multiple of 3)
Step 6: Return "Buzz" for 10 (multiple of 5)
Step 7: Return "FizzBuzz" for 15 (multiple of both)