The TDD Cycle
Red-Green-Refactor
1. RED: Write a failing test
2. GREEN: Write minimal code to pass
3. REFACTOR: Clean up while tests pass
Example: FizzBuzz
# Step 1: Write failing test
# test_fizzbuzz.py
def test_fizzbuzz_returns_number_as_string():
assert fizzbuzz(1) == '1'
# Run test: FAILS (function doesn't exist)
# Step 2: Make it pass
def fizzbuzz(n):
return str(n)
# Run test: PASSES
# Step 3: Refactor (not needed yet)
# Step 4: Write next test
def test_fizzbuzz_returns_fizz_for_3():
assert fizzbuzz(3) == 'Fizz'
# Run test: FAILS
# Step 5: Make it pass
def fizzbuzz(n):
if n % 3 == 0:
return 'Fizz'
return str(n)
# Continue cycle...
def test_fizzbuzz_returns_buzz_for_5():
assert fizzbuzz(5) == 'Buzz'
def test_fizzbuzz_returns_fizzbuzz_for_15():
assert fizzbuzz(15) == 'FizzBuzz'
Benefits of TDD
- Better design: Forces you to think about interface first
- Confidence: Changes don't break existing behavior
- Documentation: Tests show how code is used
- Debugging: Failure tells you exactly what broke
- Refactoring: Safe to improve code structure
When to Use TDD
# ✅ Good for:
# - Complex business logic
# - Bug fixes (write regression test first)
# - API design
# - Algorithms
# - Critical systems
# ❌ Less useful for:
# - Simple CRUD
# - UI code
# - Exploration/prototyping
# - Configuration
TDD in Practice
Example: String Calculator
# Step 1: Write tests first
def test_empty_string():
assert add('') == 0
def test_single_number():
assert add('1') == 1
def test_two_numbers():
assert add('1,2') == 3
def test_multiple_numbers():
assert add('1,2,3,4,5') == 15
def test_newline_separator():
assert add('1\n2,3') == 6
def test_custom_separator():
assert add('//;\n1;2') == 3
def test_negative_numbers():
with pytest.raises(ValueError) as exc:
add('-1,-2')
assert 'negatives not allowed' in str(exc.value)
assert '-1' in str(exc.value)
assert '-2' in str(exc.value)
# Step 2: Implement incrementally
def add(numbers: str) -> int:
if not numbers:
return 0
# Handle custom separator
if numbers.startswith('//'):
separator = numbers[2]
numbers = numbers[4:]
else:
separator = ','
# Replace newlines with separator
numbers = numbers.replace('\n', separator)
# Parse and validate
nums = [int(n) for n in numbers.split(separator)]
negatives = [n for n in nums if n < 0]
if negatives:
raise ValueError(f'negatives not allowed: {negatives}')
return sum(nums)
Refactoring with Tests
# Original (passes tests but messy)
def add(numbers: str) -> int:
if not numbers:
return 0
if numbers.startswith('//'):
sep = numbers[2]
nums = numbers[4:].replace('\n', sep).split(sep)
else:
nums = numbers.replace('\n', ',').split(',')
ints = []
for n in nums:
ints.append(int(n))
neg = []
for i in ints:
if i < 0:
neg.append(i)
if len(neg) > 0:
raise ValueError('negatives not allowed: ' + str(neg))
total = 0
for i in ints:
total = total + i
return total
# Refactored (same tests pass)
def add(numbers: str) -> int:
if not numbers:
return 0
separator = _get_separator(numbers)
numbers = _clean_numbers(numbers, separator)
nums = [int(n) for n in numbers.split(separator)]
_validate_no_negatives(nums)
return sum(nums)
def _get_separator(numbers: str) -> str:
if numbers.startswith('//'):
return numbers[2]
return ','
def _clean_numbers(numbers: str, separator: str) -> str:
if numbers.startswith('//'):
numbers = numbers[4:]
return numbers.replace('\n', separator)
def _validate_no_negatives(nums: list[int]) -> None:
negatives = [n for n in nums if n < 0]
if negatives:
raise ValueError(f'negatives not allowed: {negatives}')
TDD Best Practices
Test Naming
# Use descriptive names
def test_add_returns_sum_of_two_positive_numbers():
assert add(2, 3) == 5
def test_add_handles_negative_numbers():
assert add(-1, -2) == -3
# Pattern: test_<what>_<condition>_<expected>
def test_calculate_discount_for_vip_customer_returns_20_percent():
customer = Customer(is_vip=True)
assert calculate_discount(customer, 100) == 20
One Assertion Per Test
# ❌ Bad: Multiple unrelated assertions
def test_user():
user = User('Alice', 30)
assert user.name == 'Alice'
assert user.age == 30
assert user.email is None
assert user.is_active == True
# ✅ Good: One concept per test
def test_user_name():
user = User('Alice', 30)
assert user.name == 'Alice'
def test_user_age():
user = User('Alice', 30)
assert user.age == 30
Test Structure (AAA Pattern)
def test_order_total():
# Arrange
order = Order()
order.add_item('apple', 1.00, 3)
order.add_item('banana', 0.50, 2)
# Act
total = order.calculate_total()
# Assert
assert total == 4.00
F.I.R.S.T. Principles
# Fast - Tests should run quickly
def test_simple_calculation(): # Fast
assert add(1, 2) == 3
# Independent - Tests don't depend on each other
# ❌ Bad
def test_create_user():
user = create_user('Alice')
globals()['user_id'] = user.id
def test_get_user():
user = get_user(globals()['user_id'])
assert user.name == 'Alice'
# ✅ Good
def test_get_user():
user = create_user('Alice')
fetched = get_user(user.id)
assert fetched.name == 'Alice'
# Repeatable - Same result every time
# ❌ Bad
def test_with_random():
assert process(random.random()) # Unpredictable
# ✅ Good
def test_with_seed():
random.seed(42)
assert process(random.random()) # Deterministic
# Self-validating - Clear pass/fail
# ❌ Bad
def test_output():
result = calculate(1, 2)
print(result) # No assertion!
# ✅ Good
def test_output():
assert calculate(1, 2) == 3
# Timely - Written at right time (ideally before code)
Testing Edge Cases
# Test boundary conditions
def test_divide_by_zero():
with pytest.raises(ZeroDivisionError):
divide(10, 0)
def test_divide_zero():
assert divide(0, 5) == 0
def test_divide_negative():
assert divide(-10, 2) == -5
def test_divide_floats():
assert divide(1, 3) == pytest.approx(0.333, rel=1e-2)