| name | tdd |
| description | Test-Driven Development methodology for writing tests before code. Use when developing new features, fixing bugs with test cases, or ensuring high code quality through test-first approach. |
Test-Driven Development (TDD) Skill
Core Principle
Write the test first, then make it pass, then refactor.
TDD is a discipline where you:
- Write a failing test
- Write minimal code to make it pass
- Refactor while keeping tests green
This creates well-tested, well-designed code by default.
The TDD Cycle (Red-Green-Refactor)
┌─────────────────────────────────────────────────────────┐
│ TDD CYCLE │
└─────────────────────────────────────────────────────────┘
┌───────────────┐
│ 1. RED │ Write a failing test
│ ❌ Test │ (Test what you want to build)
└───────┬───────┘
│
▼
┌───────────────┐
│ 2. GREEN │ Write minimal code to pass
│ ✅ Test │ (Make it work)
└───────┬───────┘
│
▼
┌───────────────┐
│ 3. REFACTOR │ Improve code quality
│ 🔄 Clean │ (Make it right)
└───────┬───────┘
│
└─────────► Repeat for next behavior
Step 1: RED — Write a Failing Test
What to Test First
Start with the simplest, most basic behavior:
❌ Don't start with: Edge cases, error handling, complex scenarios
✅ Do start with: The happy path, core functionality, simplest use case
Example: Building a Calculator
def test_add_two_positive_numbers():
"""Test the simplest addition case"""
calc = Calculator()
result = calc.add(2, 3)
assert result == 5
Run the Test (Watch it Fail)
pytest test_calculator.py
Why this is important: Watching the test fail proves:
- The test actually runs
- The test can detect problems
- You're not testing code that already exists
Step 2: GREEN — Write Minimal Code
Goal: Make the Test Pass (Nothing More)
Write the simplest possible code to make the test pass:
class Calculator:
def add(self, a, b):
return a + b
Run the Test Again
pytest test_calculator.py
Resist the urge to:
- Add features that aren't tested
- Handle edge cases you haven't tested for
- Optimize prematurely
- Write "clever" code
At this stage: Working code > Perfect code
Step 3: REFACTOR — Improve the Code
Now Make It Better
With tests passing, you can safely improve code:
Rules for Refactoring
- Tests must stay GREEN
- No behavior changes
- Improve structure, readability, performance
- Commit after successful refactoring
Expanding Functionality
Add More Tests, One at a Time
def test_add_two_positive_numbers():
calc = Calculator()
assert calc.add(2, 3) == 5
def test_add_negative_numbers():
calc = Calculator()
assert calc.add(-1, -1) == -2
def test_add_zero():
calc = Calculator()
assert calc.add(5, 0) == 5
Pattern:
- Write one new test (RED)
- Make it pass (GREEN)
- Refactor if needed
- Repeat
TDD for Bug Fixes
When You Find a Bug
Traditional approach:
- Fix the bug
- Hope it doesn't come back
TDD approach:
- Write a test that demonstrates the bug (RED)
- Fix the code to make test pass (GREEN)
- Refactor if needed
- Bug can't return (test prevents regression)
Example: Bug Fix with TDD
def test_divide_by_zero_raises_error():
calc = Calculator()
with pytest.raises(ZeroDivisionError):
calc.divide(10, 0)
Test Organization Patterns
Arrange-Act-Assert (AAA)
Structure tests in three phases:
def test_user_registration():
email = "user@example.com"
password = "secure123"
user_service = UserService()
user = user_service.register(email, password)
assert user.email == email
assert user.password != password
assert user.id is not None
Given-When-Then (BDD Style)
Alternative pattern (same concept):
def test_shopping_cart_calculates_total():
cart = ShoppingCart()
cart.add_item(Item("Widget", price=10.00))
cart.add_item(Item("Gadget", price=15.00))
total = cart.calculate_total()
assert total == 25.00
What Makes a Good Test
Characteristics of Good Tests
✅ Fast — Run in milliseconds
✅ Independent — No dependencies on other tests
✅ Repeatable — Same result every time
✅ Self-validating — Clear pass/fail (no manual verification)
✅ Timely — Written before the code (TDD)
Test Naming Conventions
Good test names explain:
- What is being tested
- Under what conditions
- What the expected result is
def test_add_returns_sum_of_two_positive_numbers()
def test_divide_raises_error_when_divisor_is_zero()
def test_user_registration_sends_confirmation_email()
def test_add()
def test_divide()
def test_registration()
TDD Anti-Patterns to Avoid
1. Writing Tests After Code
❌ Don't:
class Calculator:
def add(self, a, b): return a + b
def subtract(self, a, b): return a - b
def multiply(self, a, b): return a * b
✅ Do:
def test_add(): ...
class Calculator:
def add(self, a, b): return a + b
2. Testing Implementation Details
❌ Don't test HOW:
def test_sort_uses_quicksort_algorithm():
sorter = Sorter()
assert sorter.algorithm == 'quicksort'
✅ Do test WHAT:
def test_sort_returns_items_in_ascending_order():
sorter = Sorter()
result = sorter.sort([3, 1, 2])
assert result == [1, 2, 3]
3. Over-Mocking
❌ Don't mock everything:
def test_process_order():
mock_db = Mock()
mock_email = Mock()
mock_payment = Mock()
mock_inventory = Mock()
✅ Do mock external dependencies only:
def test_process_order():
mock_payment_gateway = Mock()
order_processor = OrderProcessor(mock_payment_gateway)
4. One Giant Test
❌ Don't test everything in one test:
def test_user_system():
✅ Do write focused tests:
def test_user_registration_creates_account(): ...
def test_user_login_with_valid_credentials(): ...
def test_user_logout_clears_session(): ...
def test_password_reset_sends_email(): ...
TDD Workflow Example
Building a User Authentication System
Iteration 1: Basic registration
def test_register_user_with_valid_email():
auth = AuthService()
user = auth.register("user@example.com", "password123")
assert user is not None
class AuthService:
def register(self, email, password):
return User(email=email)
Iteration 2: Password hashing
def test_register_user_hashes_password():
auth = AuthService()
user = auth.register("user@example.com", "password123")
assert user.password_hash != "password123"
assert len(user.password_hash) > 20
class AuthService:
def register(self, email, password):
password_hash = bcrypt.hash(password)
return User(email=email, password_hash=password_hash)
class PasswordHasher:
@staticmethod
def hash(password):
return bcrypt.hash(password)
Iteration 3: Email validation
def test_register_user_with_invalid_email_raises_error():
auth = AuthService()
with pytest.raises(ValueError):
auth.register("not-an-email", "password123")
class AuthService:
def register(self, email, password):
if '@' not in email:
raise ValueError("Invalid email")
password_hash = PasswordHasher.hash(password)
return User(email=email, password_hash=password_hash)
class EmailValidator:
@staticmethod
def validate(email):
if '@' not in email or '.' not in email:
raise ValueError("Invalid email format")
TDD Benefits
Design Benefits
- Better API design — Writing tests first forces you to think about usage
- Modular code — Testable code is naturally decoupled
- YAGNI — You only build what's tested (what's needed)
Quality Benefits
- High test coverage — Code is tested by default
- Regression prevention — Tests catch breaking changes
- Living documentation — Tests show how code should be used
Workflow Benefits
- Confidence to refactor — Tests prove behavior unchanged
- Faster debugging — Test pinpoints exact failure
- Clear progress — Each passing test is visible progress
Integration with Other Skills
With Refactoring Skill
- TDD makes refactoring safe (tests prove behavior unchanged)
- Refactor step is built into TDD cycle
- Always refactor with GREEN tests
With Code Review Skill
- TDD code comes with tests (review checklist item)
- Tests document intended behavior
- Easier to review (test shows expected behavior)
With Git Hygiene
- Commit after each RED-GREEN-REFACTOR cycle
- Atomic commits for each new behavior
- Commit message: "test: add validation for X"
Quick Reference
TDD Cycle
- RED — Write failing test
- GREEN — Write minimal code to pass
- REFACTOR — Improve code quality
- Repeat
Test Structure (AAA)
def test_behavior():
When to TDD
✅ New features
✅ Bug fixes (test the bug first)
✅ Refactoring (tests enable safe refactoring)
✅ Learning new APIs (tests are documentation)
When NOT to TDD
❌ Prototyping/experimenting (TDD after design is clear)
❌ UI layout (use visual testing instead)
❌ Performance tuning (benchmark, don't TDD)
Remember: TDD is a discipline. It feels slow at first, but leads to faster development over time. Red-Green-Refactor. Test first, code second, refactor third. Small steps, frequent commits, always keep tests passing.