Initial commit
This commit is contained in:
532
skills/tdd-methodology-expert/references/code-smells.md
Normal file
532
skills/tdd-methodology-expert/references/code-smells.md
Normal 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.
|
||||
391
skills/tdd-methodology-expert/references/tdd-principles.md
Normal file
391
skills/tdd-methodology-expert/references/tdd-principles.md
Normal 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.
|
||||
637
skills/tdd-methodology-expert/references/testing-patterns.md
Normal file
637
skills/tdd-methodology-expert/references/testing-patterns.md
Normal 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.
|
||||
Reference in New Issue
Block a user