Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:50:59 +08:00
commit cee24bf043
13 changed files with 3252 additions and 0 deletions

View File

@@ -0,0 +1,532 @@
# Code Smells That Indicate Test-After Development
This document catalogs code smells and anti-patterns that strongly suggest tests were written after implementation rather than following TDD methodology.
## Understanding Code Smells in TDD Context
When developers write tests after code (test-after), they tend to produce different code structures than when following TDD. This is because:
1. **TDD enforces small steps**: Each test drives minimal implementation
2. **TDD encourages refactoring**: The refactor phase continuously improves structure
3. **TDD requires testability**: Code must be designed for easy testing from the start
4. **TDD prevents over-engineering**: Only write code needed to pass tests
Test-after code often shows signs of:
- Solving problems that don't exist yet (premature optimization)
- Complex structures built all at once (big bang implementation)
- Difficult-to-test designs (retrofitted testability)
- Accumulated technical debt (skipped refactoring)
## High-Severity Code Smells
### 1. Deeply Nested Conditionals
**Description**: Multiple levels of if/elif/else statements nested within each other.
**Why it indicates test-after**:
- TDD would break this down into separate, testable functions
- Each branch would have its own test, encouraging extraction
- Refactor phase would identify and eliminate deep nesting
**Example (Bad)**:
```python
def process_order(order):
if order.customer:
if order.customer.is_active:
if order.items:
if order.total > 0:
if order.payment_method:
if order.payment_method == "credit_card":
if order.customer.credit_limit >= order.total:
# Process credit card payment
return "processed"
else:
return "insufficient_credit"
else:
# Process other payment
return "processed"
else:
return "no_payment_method"
else:
return "invalid_total"
else:
return "no_items"
else:
return "inactive_customer"
else:
return "no_customer"
```
**TDD Alternative**:
```python
def process_order(order):
_validate_order(order)
_validate_customer(order.customer)
_validate_payment(order)
return _execute_payment(order)
def _validate_order(order):
if not order.items:
raise OrderValidationError("Order must have items")
if order.total <= 0:
raise OrderValidationError("Order total must be positive")
def _validate_customer(customer):
if not customer:
raise OrderValidationError("Order must have a customer")
if not customer.is_active:
raise OrderValidationError("Customer is inactive")
def _validate_payment(order):
if not order.payment_method:
raise OrderValidationError("Payment method required")
def _execute_payment(order):
payment_processor = PaymentProcessorFactory.create(order.payment_method)
return payment_processor.process(order)
```
**How to detect**: Look for 3+ levels of nested if/else statements.
### 2. Long Methods/Functions
**Description**: Methods exceeding 20-30 lines of code.
**Why it indicates test-after**:
- TDD naturally produces small functions (5-15 lines)
- Each test typically drives one small piece of functionality
- Long methods suggest big-bang implementation
**Example (Bad)**:
```python
def generate_invoice(order_id):
# 80+ lines of mixed responsibilities:
# - Database queries
# - Business logic
# - Calculations
# - Formatting
# - File generation
# - Email sending
order = db.query(Order).filter_by(id=order_id).first()
if not order:
return None
total = 0
for item in order.items:
if item.discount:
price = item.price * (1 - item.discount)
else:
price = item.price
total += price * item.quantity
tax = total * 0.1
shipping = 10 if total < 50 else 0
grand_total = total + tax + shipping
# ... 50 more lines of formatting and sending
```
**TDD Alternative**:
```python
def generate_invoice(order_id):
order = _fetch_order(order_id)
invoice_data = _calculate_invoice_totals(order)
formatted_invoice = _format_invoice(order, invoice_data)
_send_invoice(order.customer.email, formatted_invoice)
return formatted_invoice
def _fetch_order(order_id):
order = db.query(Order).filter_by(id=order_id).first()
if not order:
raise OrderNotFoundError(f"Order {order_id} not found")
return order
def _calculate_invoice_totals(order):
subtotal = sum(_calculate_line_total(item) for item in order.items)
tax = _calculate_tax(subtotal)
shipping = _calculate_shipping(subtotal)
return InvoiceTotals(subtotal, tax, shipping)
def _calculate_line_total(item):
price = item.price * (1 - item.discount) if item.discount else item.price
return price * item.quantity
```
**How to detect**: Count lines in methods. Flag anything over 20 lines.
### 3. Complex Boolean Conditions
**Description**: Conditional expressions with multiple AND/OR operators.
**Why it indicates test-after**:
- TDD encourages extracting complex conditions into named methods
- Each condition part would have its own test
- Refactor phase would identify complexity and extract it
**Example (Bad)**:
```python
if (user.age >= 18 and user.has_license and
user.years_experience >= 2 and
(user.state == "CA" or user.state == "NY") and
not user.has_violations and user.insurance_valid):
# Allow to rent car
pass
```
**TDD Alternative**:
```python
def can_rent_car(user):
return (is_eligible_driver(user) and
is_in_service_area(user) and
has_clean_record(user))
def is_eligible_driver(user):
return user.age >= 18 and user.has_license and user.years_experience >= 2
def is_in_service_area(user):
return user.state in ["CA", "NY"]
def has_clean_record(user):
return not user.has_violations and user.insurance_valid
```
**How to detect**: Count AND/OR operators. Flag conditions with 3+ logical operators.
### 4. God Objects/Classes
**Description**: Classes with too many responsibilities and methods.
**Why it indicates test-after**:
- TDD enforces Single Responsibility Principle through testing
- Each test focuses on one behavior, encouraging focused classes
- Testing god objects is painful, encouraging decomposition
**Example (Bad)**:
```python
class UserManager:
def authenticate(self, username, password): pass
def create_user(self, user_data): pass
def update_user(self, user_id, data): pass
def delete_user(self, user_id): pass
def send_welcome_email(self, user): pass
def send_password_reset(self, user): pass
def validate_email(self, email): pass
def validate_password(self, password): pass
def log_user_activity(self, user, action): pass
def generate_report(self, user_id): pass
def export_user_data(self, user_id): pass
def import_users(self, file_path): pass
# ... 20 more methods
```
**TDD Alternative**:
```python
class AuthenticationService:
def authenticate(self, username, password): pass
class UserRepository:
def create(self, user): pass
def update(self, user_id, data): pass
def delete(self, user_id): pass
def find_by_id(self, user_id): pass
class EmailService:
def send_welcome_email(self, user): pass
def send_password_reset(self, user): pass
class UserValidator:
def validate_email(self, email): pass
def validate_password(self, password): pass
class UserReportGenerator:
def generate_report(self, user_id): pass
```
**How to detect**: Count methods in class. Flag classes with 10+ methods.
## Medium-Severity Code Smells
### 5. Type Checking Instead of Polymorphism
**Description**: Using `isinstance()`, `typeof`, or type switches instead of polymorphic design.
**Why it indicates test-after**:
- TDD encourages interface-based design through mocking
- Polymorphism emerges naturally when testing behaviors
- Type checking makes testing harder, encouraging better design
**Example (Bad)**:
```python
def calculate_area(shape):
if isinstance(shape, Circle):
return 3.14159 * shape.radius ** 2
elif isinstance(shape, Rectangle):
return shape.width * shape.height
elif isinstance(shape, Triangle):
return 0.5 * shape.base * shape.height
else:
raise ValueError("Unknown shape type")
```
**TDD Alternative**:
```python
class Shape(ABC):
@abstractmethod
def calculate_area(self):
pass
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def calculate_area(self):
return 3.14159 * self.radius ** 2
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def calculate_area(self):
return self.width * self.height
# Usage:
def process_shape(shape: Shape):
return shape.calculate_area()
```
**How to detect**: Search for `isinstance()`, `typeof`, or type switch patterns.
### 6. Duplicate Code Blocks
**Description**: Same or similar code repeated in multiple places.
**Why it indicates test-after**:
- TDD's refactor phase explicitly targets duplication
- Each cycle includes time to eliminate redundancy
- Test-after often skips refactoring altogether
**Example (Bad)**:
```python
def calculate_discount_price_for_books(price, quantity):
if quantity >= 10:
discount = 0.2
elif quantity >= 5:
discount = 0.1
else:
discount = 0
return price * (1 - discount)
def calculate_discount_price_for_electronics(price, quantity):
if quantity >= 10:
discount = 0.15
elif quantity >= 5:
discount = 0.08
else:
discount = 0
return price * (1 - discount)
```
**TDD Alternative**:
```python
def calculate_discount_price(price, quantity, discount_tiers):
discount = _get_discount_for_quantity(quantity, discount_tiers)
return price * (1 - discount)
def _get_discount_for_quantity(quantity, tiers):
for min_qty, discount in sorted(tiers.items(), reverse=True):
if quantity >= min_qty:
return discount
return 0
# Usage:
BOOK_DISCOUNTS = {10: 0.2, 5: 0.1}
ELECTRONICS_DISCOUNTS = {10: 0.15, 5: 0.08}
book_price = calculate_discount_price(29.99, 12, BOOK_DISCOUNTS)
```
**How to detect**: Use code duplication analysis tools (>6 lines duplicated).
### 7. Primitive Obsession
**Description**: Using primitive types instead of small objects to represent concepts.
**Why it indicates test-after**:
- TDD encourages creating types that make tests clearer
- Value objects emerge naturally when expressing test intent
- Primitives make tests verbose and unclear
**Example (Bad)**:
```python
def create_appointment(patient_id, doctor_id, date_str, time_str, duration_mins):
# Working with primitives throughout
date = datetime.strptime(date_str, "%Y-%m-%d")
time = datetime.strptime(time_str, "%H:%M")
# ... complex validation and manipulation
```
**TDD Alternative**:
```python
@dataclass
class AppointmentTime:
date: datetime.date
time: datetime.time
duration: timedelta
def __post_init__(self):
if self.duration <= timedelta(0):
raise ValueError("Duration must be positive")
def end_time(self):
start = datetime.combine(self.date, self.time)
return start + self.duration
def create_appointment(patient_id, doctor_id, appointment_time: AppointmentTime):
# Working with rich domain objects
pass
```
**How to detect**: Look for functions with many primitive parameters (4+).
### 8. Comments Explaining What Code Does
**Description**: Comments that explain the mechanics of the code rather than the "why".
**Why it indicates test-after**:
- TDD produces self-documenting code through clear naming
- Tests serve as documentation for behavior
- Need for "what" comments suggests unclear code
**Example (Bad)**:
```python
def process(data):
# Loop through each item in data
for item in data:
# Check if item value is greater than 100
if item.value > 100:
# Multiply value by 1.5
item.value = item.value * 1.5
# Check if item is active
if item.is_active:
# Add item to results list
results.append(item)
```
**TDD Alternative**:
```python
def process_high_value_active_items(items):
return [apply_premium_pricing(item)
for item in items
if is_premium_eligible(item)]
def is_premium_eligible(item):
return item.value > 100 and item.is_active
def apply_premium_pricing(item):
item.value *= PREMIUM_MULTIPLIER
return item
```
**How to detect**: Look for comments explaining mechanics; good comments explain "why".
## Low-Severity Code Smells
### 9. Magic Numbers
**Description**: Unexplained numeric literals scattered throughout code.
**Example (Bad)**:
```python
def calculate_shipping(weight):
if weight < 5:
return 10
elif weight < 20:
return 25
else:
return 50
```
**TDD Alternative**:
```python
LIGHT_PACKAGE_THRESHOLD = 5
MEDIUM_PACKAGE_THRESHOLD = 20
LIGHT_PACKAGE_RATE = 10
MEDIUM_PACKAGE_RATE = 25
HEAVY_PACKAGE_RATE = 50
def calculate_shipping(weight):
if weight < LIGHT_PACKAGE_THRESHOLD:
return LIGHT_PACKAGE_RATE
elif weight < MEDIUM_PACKAGE_THRESHOLD:
return MEDIUM_PACKAGE_RATE
else:
return HEAVY_PACKAGE_RATE
```
### 10. Long Parameter Lists
**Description**: Methods accepting many parameters (4+).
**Example (Bad)**:
```python
def create_user(first_name, last_name, email, phone, address, city, state, zip, country):
pass
```
**TDD Alternative**:
```python
@dataclass
class UserProfile:
first_name: str
last_name: str
email: str
phone: str
@dataclass
class Address:
street: str
city: str
state: str
zip: str
country: str
def create_user(profile: UserProfile, address: Address):
pass
```
## Detection Strategy
### Automated Checks
Run these checks regularly to identify test-after patterns:
1. **Cyclomatic Complexity**: Flag methods with complexity > 10
2. **Method Length**: Flag methods > 20 lines
3. **Class Size**: Flag classes with > 10 methods
4. **Nesting Depth**: Flag code with > 3 levels of nesting
5. **Duplication**: Flag blocks of > 6 duplicated lines
6. **Parameter Count**: Flag methods with > 4 parameters
### Manual Review
Look for these patterns during code review:
1. Large commits with code and tests together
2. Tests that test implementation rather than behavior
3. Absence of refactoring commits
4. Complex code without corresponding complex tests
5. Tests that mock internal methods
## Refactoring from Test-After to TDD
If you inherit test-after code:
1. **Add characterization tests**: Cover existing behavior
2. **Identify smells**: Use automated and manual detection
3. **Extract methods**: Break down large methods
4. **Introduce types**: Replace primitives with value objects
5. **Apply patterns**: Use polymorphism, strategy, etc.
6. **Write tests first for new features**: Start TDD from now
Remember: The goal isn't perfect code, but continuously improving code quality through TDD discipline.

View File

@@ -0,0 +1,391 @@
# 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.

View File

@@ -0,0 +1,637 @@
# Testing Patterns for TDD
This document provides language-agnostic testing patterns and best practices that support effective Test-Driven Development across different programming environments.
## Test Structure Patterns
### The AAA Pattern (Arrange-Act-Assert)
The most fundamental testing pattern. Every test should follow this three-part structure:
```
Arrange: Set up the test data and preconditions
Act: Execute the behavior being tested
Assert: Verify the expected outcome
```
**Example (Python)**:
```python
def test_should_add_item_to_shopping_cart():
# Arrange
cart = ShoppingCart()
item = Item("Book", price=29.99)
# Act
cart.add_item(item)
# Assert
assert len(cart.items) == 1
assert cart.items[0] == item
```
**Example (JavaScript)**:
```javascript
test('should add item to shopping cart', () => {
// Arrange
const cart = new ShoppingCart();
const item = new Item("Book", 29.99);
// Act
cart.addItem(item);
// Assert
expect(cart.items).toHaveLength(1);
expect(cart.items[0]).toBe(item);
});
```
### Given-When-Then (BDD Style)
An alternative to AAA, commonly used in Behavior-Driven Development:
```
Given: Preconditions and context
When: The action or event
Then: Expected outcomes
```
**Example (Ruby)**:
```ruby
describe 'Shopping Cart' do
it 'adds item to cart' do
# Given a cart and an item
cart = ShoppingCart.new
item = Item.new("Book", 29.99)
# When adding the item
cart.add_item(item)
# Then the cart should contain the item
expect(cart.items.length).to eq(1)
expect(cart.items[0]).to eq(item)
end
end
```
## Test Organization Patterns
### Test Fixture Pattern
Use setup/teardown to create consistent test preconditions:
**Example (Python with pytest)**:
```python
import pytest
@pytest.fixture
def cart():
"""Create a fresh shopping cart for each test"""
return ShoppingCart()
@pytest.fixture
def sample_items():
"""Create sample items for testing"""
return [
Item("Book", 29.99),
Item("Pen", 1.99),
Item("Notebook", 5.99)
]
def test_should_calculate_correct_total(cart, sample_items):
for item in sample_items:
cart.add_item(item)
total = cart.calculate_total()
assert total == 37.97
```
**Example (JavaScript with Jest)**:
```javascript
describe('ShoppingCart', () => {
let cart;
beforeEach(() => {
cart = new ShoppingCart();
});
test('should calculate correct total', () => {
cart.addItem(new Item("Book", 29.99));
cart.addItem(new Item("Pen", 1.99));
const total = cart.calculateTotal();
expect(total).toBe(31.98);
});
});
```
### Test Builder Pattern
Create fluent APIs for complex test data setup:
**Example (Java)**:
```java
public class OrderBuilder {
private Customer customer;
private List<Item> items = new ArrayList<>();
private PaymentMethod paymentMethod;
public OrderBuilder withCustomer(String name, String email) {
this.customer = new Customer(name, email);
return this;
}
public OrderBuilder withItem(String name, double price) {
this.items.add(new Item(name, price));
return this;
}
public OrderBuilder withPayment(PaymentMethod method) {
this.paymentMethod = method;
return this;
}
public Order build() {
Order order = new Order(customer);
items.forEach(order::addItem);
order.setPaymentMethod(paymentMethod);
return order;
}
}
// Usage in tests:
@Test
public void shouldProcessOrderSuccessfully() {
Order order = new OrderBuilder()
.withCustomer("John Doe", "john@example.com")
.withItem("Book", 29.99)
.withItem("Pen", 1.99)
.withPayment(PaymentMethod.CREDIT_CARD)
.build();
OrderResult result = processor.process(order);
assertEquals(OrderStatus.COMPLETED, result.getStatus());
}
```
### Object Mother Pattern
Centralized factory for creating common test objects:
**Example (Python)**:
```python
class CustomerMother:
"""Factory for creating test customers"""
@staticmethod
def create_standard_customer():
return Customer(
name="John Doe",
email="john@example.com",
is_active=True
)
@staticmethod
def create_vip_customer():
return Customer(
name="Jane Smith",
email="jane@example.com",
is_active=True,
membership_level="VIP"
)
@staticmethod
def create_inactive_customer():
return Customer(
name="Bob Wilson",
email="bob@example.com",
is_active=False
)
# Usage in tests:
def test_should_apply_vip_discount():
customer = CustomerMother.create_vip_customer()
cart = ShoppingCart(customer)
cart.add_item(Item("Book", 100))
total = cart.calculate_total()
assert total == 80 # 20% VIP discount
```
## Assertion Patterns
### Single Assertion Principle
**Guideline**: Each test should verify one logical concept (though may have multiple assertion statements for clarity).
**Good**:
```python
def test_should_create_user_with_correct_attributes():
user = create_user("john@example.com", "John Doe")
# Multiple assertions verifying one concept: user creation
assert user.email == "john@example.com"
assert user.name == "John Doe"
assert user.is_active is True
```
**Better (when concepts are truly separate)**:
```python
def test_should_create_user_with_provided_email():
user = create_user("john@example.com", "John Doe")
assert user.email == "john@example.com"
def test_should_create_user_with_provided_name():
user = create_user("john@example.com", "John Doe")
assert user.name == "John Doe"
def test_should_create_active_user_by_default():
user = create_user("john@example.com", "John Doe")
assert user.is_active is True
```
### Custom Assertion Methods
Create domain-specific assertions for clarity:
**Example (JavaScript)**:
```javascript
function assertValidOrder(order) {
expect(order.customer).toBeDefined();
expect(order.items).not.toHaveLength(0);
expect(order.total).toBeGreaterThan(0);
expect(order.status).toBe('pending');
}
test('should create valid order from cart', () => {
const cart = createSampleCart();
const order = cart.checkout();
assertValidOrder(order);
});
```
## Test Doubles (Mocking) Patterns
### Stub Pattern
Replace dependencies with simplified implementations:
**Example (Python)**:
```python
class StubEmailService:
"""Stub that tracks calls without sending real emails"""
def __init__(self):
self.sent_emails = []
def send(self, to, subject, body):
self.sent_emails.append({
'to': to,
'subject': subject,
'body': body
})
def test_should_send_welcome_email_on_registration():
email_service = StubEmailService()
user_service = UserService(email_service)
user = user_service.register("john@example.com", "password")
assert len(email_service.sent_emails) == 1
assert email_service.sent_emails[0]['to'] == "john@example.com"
assert "Welcome" in email_service.sent_emails[0]['subject']
```
### Mock Pattern
Verify interactions with dependencies:
**Example (JavaScript with Jest)**:
```javascript
test('should call payment gateway with correct amount', () => {
const mockGateway = {
charge: jest.fn().mockResolvedValue({ success: true })
};
const processor = new PaymentProcessor(mockGateway);
processor.processPayment(customer, 100.00);
expect(mockGateway.charge).toHaveBeenCalledWith(
customer.paymentToken,
100.00
);
});
```
### Fake Pattern
Provide working implementations with shortcuts:
**Example (Python)**:
```python
class FakeUserRepository:
"""In-memory repository for testing"""
def __init__(self):
self.users = {}
self.next_id = 1
def save(self, user):
if not user.id:
user.id = self.next_id
self.next_id += 1
self.users[user.id] = user
return user
def find_by_id(self, user_id):
return self.users.get(user_id)
def test_should_persist_user_with_generated_id():
repo = FakeUserRepository()
user = User(name="John")
saved_user = repo.save(user)
assert saved_user.id is not None
assert repo.find_by_id(saved_user.id) == saved_user
```
## Parameterized Testing Pattern
Test multiple cases with the same structure:
**Example (Python with pytest)**:
```python
import pytest
@pytest.mark.parametrize("input,expected", [
(0, 0),
(1, 1),
(2, 2),
(3, 6),
(4, 24),
(5, 120),
])
def test_factorial_calculation(input, expected):
result = factorial(input)
assert result == expected
```
**Example (JavaScript with Jest)**:
```javascript
test.each([
[0, 0],
[1, 1],
[2, 2],
[3, 6],
[4, 24],
[5, 120],
])('factorial(%i) should equal %i', (input, expected) => {
const result = factorial(input);
expect(result).toBe(expected);
});
```
## Exception Testing Pattern
Test error conditions explicitly:
**Example (Python)**:
```python
def test_should_raise_error_when_withdrawing_too_much():
account = Account(balance=100)
with pytest.raises(InsufficientFundsError) as exc_info:
account.withdraw(150)
assert "Insufficient funds" in str(exc_info.value)
assert exc_info.value.available == 100
assert exc_info.value.requested == 150
```
**Example (JavaScript)**:
```javascript
test('should throw error when withdrawing too much', () => {
const account = new Account(100);
expect(() => {
account.withdraw(150);
}).toThrow(InsufficientFundsError);
expect(() => {
account.withdraw(150);
}).toThrow('Insufficient funds');
});
```
## Property-Based Testing Pattern
Test properties that should always hold:
**Example (Python with hypothesis)**:
```python
from hypothesis import given
import hypothesis.strategies as st
@given(st.lists(st.integers()))
def test_reversing_twice_gives_original(lst):
result = reverse(reverse(lst))
assert result == lst
@given(st.integers(min_value=0))
def test_factorial_is_positive(n):
result = factorial(n)
assert result > 0
```
## State-Based vs. Interaction-Based Testing
### State-Based Testing (Preferred)
Verify final state rather than how it was achieved:
**Example**:
```python
def test_should_remove_item_from_cart():
cart = ShoppingCart()
item = Item("Book", 29.99)
cart.add_item(item)
cart.remove_item(item)
assert len(cart.items) == 0 # Verify state
```
### Interaction-Based Testing
Verify interactions when state is not observable:
**Example**:
```python
def test_should_log_failed_login_attempt():
logger = Mock()
auth = AuthService(logger)
auth.login("user", "wrong_password")
logger.warning.assert_called_once_with(
"Failed login attempt for user: user"
)
```
## Test Naming Patterns
### Should-Style Naming
```
test_should_<expected_behavior>_when_<condition>
```
**Examples**:
- `test_should_return_empty_list_when_no_matches_found`
- `test_should_throw_exception_when_amount_is_negative`
- `test_should_apply_discount_when_quantity_exceeds_ten`
### Behavior-Style Naming
```
test_<subject>_<scenario>_<expected_result>
```
**Examples**:
- `test_cart_with_multiple_items_calculates_correct_total`
- `test_user_registration_with_invalid_email_fails`
- `test_payment_processing_with_insufficient_funds_raises_error`
## Test Data Patterns
### Obvious Data
Use self-documenting test data:
**Bad**:
```python
def test_calculation():
result = calculate(10, 5, 3)
assert result == 8
```
**Good**:
```python
def test_should_calculate_average_correctly():
value1 = 10
value2 = 20
value3 = 30
expected_average = 20
result = calculate_average([value1, value2, value3])
assert result == expected_average
```
### Boundary Value Testing
Test edges of valid ranges:
```python
def test_age_validation():
validator = AgeValidator(min_age=18, max_age=100)
# Below minimum
assert not validator.is_valid(17)
# At minimum boundary
assert validator.is_valid(18)
# Normal value
assert validator.is_valid(50)
# At maximum boundary
assert validator.is_valid(100)
# Above maximum
assert not validator.is_valid(101)
```
## TDD-Specific Patterns
### Triangulation
Build generality through multiple test cases:
**Step 1 - Specific case**:
```python
def test_should_add_two_numbers():
assert add(2, 3) == 5
# Implementation:
def add(a, b):
return 5 # Hardcoded - simplest thing that works
```
**Step 2 - Add another case to force generalization**:
```python
def test_should_add_two_numbers():
assert add(2, 3) == 5
def test_should_add_different_numbers():
assert add(4, 7) == 11
# Implementation:
def add(a, b):
return a + b # Now forced to generalize
```
### Fake It Till You Make It
Start with simple/hardcoded implementations:
```python
# Test
def test_should_return_greeting():
assert greet("World") == "Hello, World!"
# First implementation (fake it)
def greet(name):
return "Hello, World!"
# Add another test to force real implementation
def test_should_return_greeting_with_different_name():
assert greet("Alice") == "Hello, Alice!"
# Real implementation
def greet(name):
return f"Hello, {name}!"
```
### Obvious Implementation
When the implementation is obvious, just write it:
```python
# Test
def test_should_calculate_rectangle_area():
assert calculate_area(width=5, height=3) == 15
# Implementation (obvious, no need to fake)
def calculate_area(width, height):
return width * height
```
## Summary: Testing Pattern Selection
- **Use AAA** for clarity in most tests
- **Use fixtures** to eliminate setup duplication
- **Use builders** for complex object creation
- **Use test doubles** when dependencies are expensive or unpredictable
- **Prefer state-based** testing over interaction-based
- **Use parameterized tests** for multiple similar cases
- **Test boundaries** explicitly
- **Name tests** to describe behavior
- **Keep tests independent** of each other
- **Make tests readable** - they're documentation
Remember: Tests are first-class citizens in TDD. Treat them with the same care as production code.