392 lines
12 KiB
Markdown
392 lines
12 KiB
Markdown
# Test-Driven Development (TDD) Principles
|
|
|
|
This document provides comprehensive guidance on TDD methodology, the Red-Green-Refactor cycle, and how to apply TDD effectively across different programming contexts.
|
|
|
|
## Core Philosophy
|
|
|
|
Test-Driven Development is a software development methodology where tests are written before the production code. The fundamental principle is: **Write failing tests first, then write code to make them pass.**
|
|
|
|
### Why TDD?
|
|
|
|
1. **Design Pressure**: Writing tests first forces you to think about the API design before implementation
|
|
2. **Regression Safety**: Every feature is protected by tests from the moment it's created
|
|
3. **Living Documentation**: Tests serve as executable examples of how the code should be used
|
|
4. **Confidence to Refactor**: Comprehensive test coverage enables safe code improvements
|
|
5. **Better Code Structure**: TDD naturally produces more modular, testable, and maintainable code
|
|
|
|
## The Red-Green-Refactor Cycle
|
|
|
|
TDD follows a strict three-phase cycle that must be repeated for every small piece of functionality:
|
|
|
|
### 🔴 Red Phase: Write a Failing Test
|
|
|
|
**Purpose**: Define the desired behavior before implementation exists.
|
|
|
|
**Process**:
|
|
1. Write a test that expresses the desired behavior
|
|
2. Run the test and verify it fails (it must fail for the right reason)
|
|
3. The failure confirms the test is actually testing something
|
|
|
|
**Key Principles**:
|
|
- Write the simplest test that expresses one behavior
|
|
- Test should be readable and clearly express intent
|
|
- Focus on behavior, not implementation
|
|
- Use descriptive test names (e.g., `test_should_return_empty_list_when_no_items_match`)
|
|
|
|
**Example (Python)**:
|
|
```python
|
|
# Red Phase: Test written first, will fail
|
|
def test_should_calculate_total_price_with_tax():
|
|
cart = ShoppingCart()
|
|
cart.add_item(Item("Book", price=10.00))
|
|
|
|
total = cart.calculate_total(tax_rate=0.1)
|
|
|
|
assert total == 11.00 # This will fail - method doesn't exist yet
|
|
```
|
|
|
|
**Red Phase Checklist**:
|
|
- [ ] Test clearly expresses desired behavior?
|
|
- [ ] Test name is descriptive and behavior-focused?
|
|
- [ ] Test actually fails when run?
|
|
- [ ] Failure message is clear about what's missing?
|
|
|
|
### 🟢 Green Phase: Make the Test Pass
|
|
|
|
**Purpose**: Write the minimal code necessary to make the failing test pass.
|
|
|
|
**Process**:
|
|
1. Write just enough production code to make the test pass
|
|
2. Take shortcuts if needed (you'll refactor later)
|
|
3. Run the test and verify it passes
|
|
4. Don't add extra features or abstractions yet
|
|
|
|
**Key Principles**:
|
|
- Write the simplest implementation that makes the test pass
|
|
- Don't worry about code quality yet (that's the refactor phase)
|
|
- Resist the urge to add features not tested
|
|
- It's okay to hard-code values initially (refactor will generalize)
|
|
|
|
**Example (Python)**:
|
|
```python
|
|
# Green Phase: Minimal implementation to pass the test
|
|
class ShoppingCart:
|
|
def __init__(self):
|
|
self.items = []
|
|
|
|
def add_item(self, item):
|
|
self.items.append(item)
|
|
|
|
def calculate_total(self, tax_rate):
|
|
# Minimal implementation - just make it work
|
|
subtotal = sum(item.price for item in self.items)
|
|
return subtotal * (1 + tax_rate)
|
|
|
|
class Item:
|
|
def __init__(self, name, price):
|
|
self.name = name
|
|
self.price = price
|
|
```
|
|
|
|
**Green Phase Checklist**:
|
|
- [ ] Test now passes?
|
|
- [ ] Only added code necessary to pass the test?
|
|
- [ ] No extra features or premature optimization?
|
|
- [ ] All previously passing tests still pass?
|
|
|
|
### 🔵 Refactor Phase: Improve the Code
|
|
|
|
**Purpose**: Improve code quality while maintaining passing tests.
|
|
|
|
**Process**:
|
|
1. Look for duplication, poor names, or structural issues
|
|
2. Refactor the code to improve quality
|
|
3. Run tests after each small change to ensure they still pass
|
|
4. Continue until code is clean and maintainable
|
|
|
|
**Key Principles**:
|
|
- Never refactor with failing tests
|
|
- Make small, incremental changes
|
|
- Run tests frequently during refactoring
|
|
- Improve both test code and production code
|
|
- Apply design patterns and best practices
|
|
|
|
**Example (Python)**:
|
|
```python
|
|
# Refactor Phase: Improve structure and readability
|
|
class ShoppingCart:
|
|
def __init__(self):
|
|
self._items = []
|
|
|
|
def add_item(self, item):
|
|
self._items.append(item)
|
|
|
|
def calculate_total(self, tax_rate=0.0):
|
|
subtotal = self._calculate_subtotal()
|
|
tax_amount = subtotal * tax_rate
|
|
return subtotal + tax_amount
|
|
|
|
def _calculate_subtotal(self):
|
|
return sum(item.price for item in self._items)
|
|
|
|
|
|
class Item:
|
|
def __init__(self, name, price):
|
|
if price < 0:
|
|
raise ValueError("Price cannot be negative")
|
|
self.name = name
|
|
self.price = price
|
|
```
|
|
|
|
**Refactor Phase Checklist**:
|
|
- [ ] Code is DRY (Don't Repeat Yourself)?
|
|
- [ ] Names are clear and expressive?
|
|
- [ ] Functions/methods are focused and small?
|
|
- [ ] Code follows SOLID principles?
|
|
- [ ] All tests still pass?
|
|
- [ ] Test code is also clean and maintainable?
|
|
|
|
## TDD Rhythm and Cadence
|
|
|
|
### The Cycle Duration
|
|
|
|
A complete Red-Green-Refactor cycle should be **very short** - typically 2-10 minutes:
|
|
- Red: 1-2 minutes to write a small test
|
|
- Green: 1-5 minutes to make it pass
|
|
- Refactor: 1-3 minutes to clean up
|
|
|
|
If cycles are taking longer, the tests are too large. Break them into smaller pieces.
|
|
|
|
### Commit Strategy
|
|
|
|
Commit at the end of each complete Red-Green-Refactor cycle:
|
|
- After Red: Don't commit failing tests
|
|
- After Green: Can commit if needed, but prefer to refactor first
|
|
- After Refactor: **Commit here** - you have working, clean code with tests
|
|
|
|
### Incremental Development
|
|
|
|
Build features incrementally, one test at a time:
|
|
|
|
**Bad (Big Bang)**:
|
|
```
|
|
Write comprehensive test suite → Implement entire feature → Debug for hours
|
|
```
|
|
|
|
**Good (Incremental)**:
|
|
```
|
|
Test 1 → Implement 1 → Refactor →
|
|
Test 2 → Implement 2 → Refactor →
|
|
Test 3 → Implement 3 → Refactor → ...
|
|
```
|
|
|
|
## TDD Best Practices
|
|
|
|
### 1. Test Behavior, Not Implementation
|
|
|
|
**Bad**:
|
|
```python
|
|
def test_uses_quicksort_algorithm():
|
|
sorter = Sorter()
|
|
# Testing internal implementation detail
|
|
assert sorter._partition_method == "quicksort"
|
|
```
|
|
|
|
**Good**:
|
|
```python
|
|
def test_should_return_sorted_list_in_ascending_order():
|
|
sorter = Sorter()
|
|
unsorted = [3, 1, 4, 1, 5]
|
|
|
|
result = sorter.sort(unsorted)
|
|
|
|
assert result == [1, 1, 3, 4, 5]
|
|
```
|
|
|
|
### 2. One Assertion Per Test (Usually)
|
|
|
|
**Bad**:
|
|
```python
|
|
def test_user_registration():
|
|
user = register_user("john@example.com", "password123")
|
|
|
|
assert user.email == "john@example.com"
|
|
assert user.is_active == True
|
|
assert user.created_at is not None
|
|
assert user.id is not None
|
|
# Too many concerns in one test
|
|
```
|
|
|
|
**Good**:
|
|
```python
|
|
def test_should_create_user_with_provided_email():
|
|
user = register_user("john@example.com", "password123")
|
|
assert user.email == "john@example.com"
|
|
|
|
def test_should_create_active_user_by_default():
|
|
user = register_user("john@example.com", "password123")
|
|
assert user.is_active == True
|
|
|
|
def test_should_assign_creation_timestamp_to_new_user():
|
|
user = register_user("john@example.com", "password123")
|
|
assert user.created_at is not None
|
|
```
|
|
|
|
### 3. Arrange-Act-Assert (AAA) Pattern
|
|
|
|
Structure every test with three clear sections:
|
|
|
|
```python
|
|
def test_should_withdraw_amount_from_account_balance():
|
|
# Arrange: Set up test data and preconditions
|
|
account = Account(initial_balance=100)
|
|
|
|
# Act: Execute the behavior being tested
|
|
account.withdraw(30)
|
|
|
|
# Assert: Verify the expected outcome
|
|
assert account.balance == 70
|
|
```
|
|
|
|
### 4. Test Names Should Be Descriptive
|
|
|
|
**Bad**: `test_user()`, `test_1()`, `test_validation()`
|
|
|
|
**Good**:
|
|
- `test_should_reject_user_registration_with_invalid_email()`
|
|
- `test_should_return_empty_list_when_database_has_no_records()`
|
|
- `test_should_throw_exception_when_withdrawal_exceeds_balance()`
|
|
|
|
### 5. Keep Tests Fast
|
|
|
|
- Unit tests should run in milliseconds
|
|
- Avoid file I/O, network calls, and databases in unit tests
|
|
- Use mocks/stubs for external dependencies
|
|
- Slow tests discourage running them frequently
|
|
|
|
### 6. Keep Tests Independent
|
|
|
|
Each test should be able to run in isolation:
|
|
- Don't rely on test execution order
|
|
- Don't share state between tests
|
|
- Use setup/teardown to create clean state for each test
|
|
|
|
## Common TDD Mistakes
|
|
|
|
### Mistake 1: Writing Tests After Code
|
|
|
|
**Problem**: Writing tests after implementation defeats the purpose of TDD.
|
|
|
|
**Why it matters**: You lose the design benefits of TDD and tests become implementation-focused rather than behavior-focused.
|
|
|
|
**Solution**: Discipline. Always write the test first, even if it feels slower initially.
|
|
|
|
### Mistake 2: Tests That Are Too Large
|
|
|
|
**Problem**: Writing comprehensive tests that cover too much functionality.
|
|
|
|
**Why it matters**: Large tests lead to large implementations, losing the incremental nature of TDD.
|
|
|
|
**Solution**: Break down tests into smallest possible behavioral units. If a test takes more than 5 minutes to implement, it's too big.
|
|
|
|
### Mistake 3: Skipping the Refactor Phase
|
|
|
|
**Problem**: Moving to the next test immediately after green without refactoring.
|
|
|
|
**Why it matters**: Technical debt accumulates quickly, leading to unmaintainable code.
|
|
|
|
**Solution**: Always spend time in the refactor phase. Code quality is not optional.
|
|
|
|
### Mistake 4: Testing Implementation Details
|
|
|
|
**Problem**: Tests that check how code works internally rather than what it produces.
|
|
|
|
**Why it matters**: Implementation-focused tests are fragile and prevent refactoring.
|
|
|
|
**Solution**: Focus tests on public interfaces and observable behavior.
|
|
|
|
### Mistake 5: Incomplete Red Phase
|
|
|
|
**Problem**: Not running the test to verify it actually fails.
|
|
|
|
**Why it matters**: You might write a test that always passes (false positive).
|
|
|
|
**Solution**: Always verify the test fails before implementing. Watch it turn from red to green.
|
|
|
|
## TDD in Different Contexts
|
|
|
|
### Unit Testing (Primary TDD Focus)
|
|
|
|
- Test individual functions/methods in isolation
|
|
- Fast execution (milliseconds)
|
|
- Mock external dependencies
|
|
- High coverage of edge cases
|
|
|
|
### Integration Testing (TDD Can Apply)
|
|
|
|
- Test interactions between components
|
|
- Slower execution (seconds)
|
|
- Use real dependencies where practical
|
|
- Focus on critical integration points
|
|
|
|
### Acceptance Testing (BDD/TDD Hybrid)
|
|
|
|
- Test complete user scenarios
|
|
- Written in behavior-focused language
|
|
- Slower execution (seconds to minutes)
|
|
- Validate business requirements
|
|
|
|
## Measuring TDD Effectiveness
|
|
|
|
### Good TDD Indicators
|
|
|
|
1. **Test Count**: Tests outnumber production code files
|
|
2. **Test Coverage**: High line/branch coverage (>80%)
|
|
3. **Commit Frequency**: Small, frequent commits
|
|
4. **Code Structure**: Small functions, low complexity
|
|
5. **Refactoring Confidence**: Easy to change code without fear
|
|
|
|
### Poor TDD Indicators
|
|
|
|
1. **Test-After Pattern**: Tests written in bulk after features
|
|
2. **Low Coverage**: Large portions of code untested
|
|
3. **Complex Code**: Long methods, deep nesting
|
|
4. **Fragile Tests**: Tests break with minor refactoring
|
|
5. **Fear of Change**: Reluctance to modify code
|
|
|
|
## TDD and Code Quality
|
|
|
|
### Code Characteristics of TDD
|
|
|
|
Well-executed TDD produces code with:
|
|
|
|
✅ **Small Functions**: Typically 5-20 lines
|
|
✅ **Flat Structure**: Minimal nesting (1-2 levels max)
|
|
✅ **Single Responsibility**: Each function does one thing
|
|
✅ **Clear Naming**: Descriptive names that explain purpose
|
|
✅ **Low Coupling**: Components loosely connected
|
|
✅ **High Cohesion**: Related functionality grouped together
|
|
✅ **Testable Design**: Easy to test in isolation
|
|
|
|
### Code Smells Indicating Test-After Development
|
|
|
|
❌ **Long Methods**: Functions over 30 lines
|
|
❌ **Deep Nesting**: 3+ levels of if/else/for statements
|
|
❌ **Complex Conditionals**: Multiple AND/OR in one condition
|
|
❌ **Type Checking**: Using `isinstance()` or `typeof`
|
|
❌ **God Objects**: Classes with too many responsibilities
|
|
❌ **Tight Coupling**: Components highly dependent on each other
|
|
❌ **Poor Naming**: Generic names like `data`, `process`, `manager`
|
|
|
|
## Summary: The TDD Mindset
|
|
|
|
TDD is not just about testing - it's a design methodology:
|
|
|
|
1. **Tests Drive Design**: Let test requirements shape your API
|
|
2. **Incremental Progress**: Build features one small test at a time
|
|
3. **Continuous Refinement**: Always improve code quality through refactoring
|
|
4. **Fast Feedback**: Run tests constantly to catch issues immediately
|
|
5. **Confidence**: Trust your tests to enable bold refactoring
|
|
|
|
Remember: **Red-Green-Refactor** is not optional. Follow the cycle religiously, and your code quality will improve dramatically.
|