feat(rules): add strict TDD enforcement ruleset with AI patterns
This commit is contained in:
476
rules/concerns/tdd.md
Normal file
476
rules/concerns/tdd.md
Normal file
@@ -0,0 +1,476 @@
|
||||
# Test-Driven Development (Strict Enforcement)
|
||||
|
||||
## Critical Rules (MUST follow)
|
||||
|
||||
**NEVER write production code without a failing test first.**
|
||||
**ALWAYS follow the red-green-refactor cycle. No exceptions.**
|
||||
**NEVER skip the refactor step. Code quality is mandatory.**
|
||||
**ALWAYS commit after green, never commit red tests.**
|
||||
|
||||
---
|
||||
|
||||
## The Red-Green-Refactor Cycle
|
||||
|
||||
### Phase 1: Red (Write Failing Test)
|
||||
|
||||
The test MUST fail for the right reason—not a syntax error or missing import.
|
||||
|
||||
```python
|
||||
# CORRECT: Test fails because behavior doesn't exist yet
|
||||
def test_calculate_discount_for_premium_members():
|
||||
user = User(tier="premium")
|
||||
cart = Cart(items=[Item(price=100)])
|
||||
|
||||
discount = calculate_discount(user, cart)
|
||||
|
||||
assert discount == 10 # Fails: calculate_discount not implemented
|
||||
|
||||
# INCORRECT: Test fails for wrong reason (will pass accidentally)
|
||||
def test_calculate_discount():
|
||||
discount = calculate_discount() # Fails: missing required args
|
||||
assert discount is not None
|
||||
```
|
||||
|
||||
**Red Phase Checklist:**
|
||||
- [ ] Test describes ONE behavior
|
||||
- [ ] Test name clearly states expected outcome
|
||||
- [ ] Test fails for the intended reason
|
||||
- [ ] Error message is meaningful
|
||||
|
||||
### Phase 2: Green (Write Minimum Code)
|
||||
|
||||
Write the MINIMUM code to make the test pass. Do not implement future features.
|
||||
|
||||
```python
|
||||
# CORRECT: Minimum implementation
|
||||
def calculate_discount(user, cart):
|
||||
if user.tier == "premium":
|
||||
return 10
|
||||
return 0
|
||||
|
||||
# INCORRECT: Over-engineering for future needs
|
||||
def calculate_discount(user, cart):
|
||||
discounts = {
|
||||
"premium": 10,
|
||||
"gold": 15, # Not tested
|
||||
"silver": 5, # Not tested
|
||||
"basic": 0 # Not tested
|
||||
}
|
||||
return discounts.get(user.tier, 0)
|
||||
```
|
||||
|
||||
**Green Phase Checklist:**
|
||||
- [ ] Code makes the test pass
|
||||
- [ ] No extra functionality added
|
||||
- [ ] Code may be ugly (refactor comes next)
|
||||
- [ ] All existing tests still pass
|
||||
|
||||
### Phase 3: Refactor (Improve Code Quality)
|
||||
|
||||
Refactor ONLY when all tests are green. Make small, incremental changes.
|
||||
|
||||
```python
|
||||
# BEFORE (Green but messy)
|
||||
def calculate_discount(user, cart):
|
||||
if user.tier == "premium":
|
||||
return 10
|
||||
return 0
|
||||
|
||||
# AFTER (Refactored)
|
||||
DISCOUNT_RATES = {"premium": 0.10}
|
||||
|
||||
def calculate_discount(user, cart):
|
||||
rate = DISCOUNT_RATES.get(user.tier, 0)
|
||||
return int(cart.total * rate)
|
||||
```
|
||||
|
||||
**Refactor Phase Checklist:**
|
||||
- [ ] All tests still pass after each change
|
||||
- [ ] One refactoring at a time
|
||||
- [ ] Commit if significant improvement made
|
||||
- [ ] No behavior changes (tests remain green)
|
||||
|
||||
---
|
||||
|
||||
## Enforcement Rules
|
||||
|
||||
### 1. Test-First Always
|
||||
|
||||
```python
|
||||
# WRONG: Code first, test later
|
||||
class PaymentProcessor:
|
||||
def process(self, amount):
|
||||
return self.gateway.charge(amount)
|
||||
|
||||
# Then write test... (TOO LATE!)
|
||||
|
||||
# CORRECT: Test first
|
||||
def test_process_payment_charges_gateway():
|
||||
mock_gateway = MockGateway()
|
||||
processor = PaymentProcessor(gateway=mock_gateway)
|
||||
|
||||
processor.process(100)
|
||||
|
||||
assert mock_gateway.charged_amount == 100
|
||||
```
|
||||
|
||||
### 2. No Commented-Out Tests
|
||||
|
||||
```python
|
||||
# WRONG: Commented test hides failing behavior
|
||||
# def test_refund_processing():
|
||||
# # TODO: fix this later
|
||||
# assert False
|
||||
|
||||
# CORRECT: Use skip with reason
|
||||
@pytest.mark.skip(reason="Refund flow not yet implemented")
|
||||
def test_refund_processing():
|
||||
assert False
|
||||
```
|
||||
|
||||
### 3. Commit Hygiene
|
||||
|
||||
```bash
|
||||
# WRONG: Committing with failing tests
|
||||
git commit -m "WIP: adding payment"
|
||||
# Tests fail in CI
|
||||
|
||||
# CORRECT: Only commit green
|
||||
git commit -m "Add payment processing"
|
||||
# All tests pass locally and in CI
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## AI-Assisted TDD Patterns
|
||||
|
||||
### Pattern 1: Explicit Test Request
|
||||
|
||||
When working with AI assistants, request tests explicitly:
|
||||
|
||||
```
|
||||
CORRECT PROMPT:
|
||||
"Write a failing test for calculating user discounts based on tier.
|
||||
Then implement the minimum code to make it pass."
|
||||
|
||||
INCORRECT PROMPT:
|
||||
"Implement a discount calculator with tier support."
|
||||
```
|
||||
|
||||
### Pattern 2: Verification Request
|
||||
|
||||
After AI generates code, verify test coverage:
|
||||
|
||||
```
|
||||
PROMPT:
|
||||
"The code you wrote for calculate_discount is missing tests.
|
||||
First, show me a failing test for the edge case where cart is empty.
|
||||
Then make it pass with minimum code."
|
||||
```
|
||||
|
||||
### Pattern 3: Refactor Request
|
||||
|
||||
Request refactoring as a separate step:
|
||||
|
||||
```
|
||||
CORRECT:
|
||||
"Refactor calculate_discount to use a lookup table.
|
||||
Run tests after each change."
|
||||
|
||||
INCORRECT:
|
||||
"Refactor and add new features at the same time."
|
||||
```
|
||||
|
||||
### Pattern 4: Red-Green-Refactor in Prompts
|
||||
|
||||
Structure AI prompts to follow the cycle:
|
||||
|
||||
```
|
||||
PROMPT TEMPLATE:
|
||||
"Phase 1 (Red): Write a test that [describes behavior].
|
||||
The test should fail because [reason].
|
||||
Show me the failing test output.
|
||||
|
||||
Phase 2 (Green): Write the minimum code to pass this test.
|
||||
No extra features.
|
||||
|
||||
Phase 3 (Refactor): Review the code. Suggest improvements.
|
||||
I'll approve before you apply changes."
|
||||
```
|
||||
|
||||
### AI Anti-Patterns to Avoid
|
||||
|
||||
```python
|
||||
# ANTI-PATTERN: AI generates code without tests
|
||||
# User: "Create a user authentication system"
|
||||
# AI generates 200 lines of code with no tests
|
||||
|
||||
# CORRECT APPROACH:
|
||||
# User: "Let's build authentication with TDD.
|
||||
# First, write a failing test for successful login."
|
||||
|
||||
# ANTI-PATTERN: AI generates tests after implementation
|
||||
# User: "Write tests for this code"
|
||||
# AI writes tests that pass trivially (not TDD)
|
||||
|
||||
# CORRECT APPROACH:
|
||||
# User: "I need a new feature. Write the failing test first."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Legacy Code Strategy
|
||||
|
||||
### 1. Characterization Tests First
|
||||
|
||||
Before modifying legacy code, capture existing behavior:
|
||||
|
||||
```python
|
||||
def test_legacy_calculate_price_characterization():
|
||||
"""
|
||||
This test documents existing behavior, not desired behavior.
|
||||
Do not change expected values without understanding impact.
|
||||
"""
|
||||
# Given: Current production inputs
|
||||
order = Order(items=[Item(price=100, quantity=2)])
|
||||
|
||||
# When: Execute legacy code
|
||||
result = legacy_calculate_price(order)
|
||||
|
||||
# Then: Capture ACTUAL output (even if wrong)
|
||||
assert result == 215 # Includes mystery 7.5% surcharge
|
||||
```
|
||||
|
||||
### 2. Strangler Fig Pattern
|
||||
|
||||
```python
|
||||
# Step 1: Write test for new behavior
|
||||
def test_calculate_price_with_new_algorithm():
|
||||
order = Order(items=[Item(price=100, quantity=2)])
|
||||
result = calculate_price_v2(order)
|
||||
assert result == 200 # No mystery surcharge
|
||||
|
||||
# Step 2: Implement new code with TDD
|
||||
def calculate_price_v2(order):
|
||||
return sum(item.price * item.quantity for item in order.items)
|
||||
|
||||
# Step 3: Route new requests to new code
|
||||
def calculate_price(order):
|
||||
if order.use_new_pricing:
|
||||
return calculate_price_v2(order)
|
||||
return legacy_calculate_price(order)
|
||||
|
||||
# Step 4: Gradually migrate, removing legacy path
|
||||
```
|
||||
|
||||
### 3. Safe Refactoring Sequence
|
||||
|
||||
```python
|
||||
# 1. Add characterization tests
|
||||
# 2. Extract method (tests stay green)
|
||||
# 3. Add unit tests for extracted method
|
||||
# 4. Refactor extracted method with TDD
|
||||
# 5. Inline or delete old method
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Integration Test TDD
|
||||
|
||||
### Outside-In (London School)
|
||||
|
||||
```python
|
||||
# 1. Write acceptance test (fails end-to-end)
|
||||
def test_user_can_complete_purchase():
|
||||
user = create_user()
|
||||
add_item_to_cart(user, item)
|
||||
|
||||
result = complete_purchase(user)
|
||||
|
||||
assert result.status == "success"
|
||||
assert user.has_receipt()
|
||||
|
||||
# 2. Drop down to unit test for first component
|
||||
def test_cart_calculates_total():
|
||||
cart = Cart()
|
||||
cart.add(Item(price=100))
|
||||
|
||||
assert cart.total == 100
|
||||
|
||||
# 3. Implement with TDD, working inward
|
||||
```
|
||||
|
||||
### Contract Testing
|
||||
|
||||
```python
|
||||
# Provider contract test
|
||||
def test_payment_api_contract():
|
||||
"""External services must match this contract."""
|
||||
response = client.post("/payments", json={
|
||||
"amount": 100,
|
||||
"currency": "USD"
|
||||
})
|
||||
|
||||
assert response.status_code == 201
|
||||
assert "transaction_id" in response.json()
|
||||
|
||||
# Consumer contract test
|
||||
def test_payment_gateway_contract():
|
||||
"""We expect the gateway to return transaction IDs."""
|
||||
mock_gateway = MockPaymentGateway()
|
||||
mock_gateway.expect_charge(amount=100).and_return(
|
||||
transaction_id="tx_123"
|
||||
)
|
||||
|
||||
result = process_payment(mock_gateway, amount=100)
|
||||
|
||||
assert result.transaction_id == "tx_123"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Refactoring Rules
|
||||
|
||||
### Rule 1: Refactor Only When Green
|
||||
|
||||
```python
|
||||
# WRONG: Refactoring with failing test
|
||||
def test_new_feature():
|
||||
assert False # Failing
|
||||
|
||||
def existing_code():
|
||||
# Refactoring here is DANGEROUS
|
||||
pass
|
||||
|
||||
# CORRECT: All tests pass before refactoring
|
||||
def existing_code():
|
||||
# Safe to refactor now
|
||||
pass
|
||||
```
|
||||
|
||||
### Rule 2: One Refactoring at a Time
|
||||
|
||||
```python
|
||||
# WRONG: Multiple refactorings at once
|
||||
def process_order(order):
|
||||
# Changed: variable name
|
||||
# Changed: extracted method
|
||||
# Changed: added caching
|
||||
# Which broke it? Who knows.
|
||||
pass
|
||||
|
||||
# CORRECT: One change, test, commit
|
||||
# Commit 1: Rename variable
|
||||
# Commit 2: Extract method
|
||||
# Commit 3: Add caching
|
||||
```
|
||||
|
||||
### Rule 3: Baby Steps
|
||||
|
||||
```python
|
||||
# WRONG: Large refactoring
|
||||
# Before: 500-line monolith
|
||||
# After: 10 new classes
|
||||
# Risk: Too high
|
||||
|
||||
# CORRECT: Extract one method at a time
|
||||
# Step 1: Extract calculate_total (commit)
|
||||
# Step 2: Extract validate_items (commit)
|
||||
# Step 3: Extract apply_discounts (commit)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Test Quality Gates
|
||||
|
||||
### Pre-Commit Hooks
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
# .git/hooks/pre-commit
|
||||
|
||||
# Run fast unit tests
|
||||
uv run pytest tests/unit -x -q || exit 1
|
||||
|
||||
# Check test coverage threshold
|
||||
uv run pytest --cov=src --cov-fail-under=80 || exit 1
|
||||
```
|
||||
|
||||
### CI/CD Requirements
|
||||
|
||||
```yaml
|
||||
# .github/workflows/test.yml
|
||||
- name: Run Tests
|
||||
run: |
|
||||
pytest --cov=src --cov-report=xml --cov-fail-under=80
|
||||
|
||||
- name: Check Test Quality
|
||||
run: |
|
||||
# Fail if new code lacks tests
|
||||
diff-cover coverage.xml --fail-under=80
|
||||
```
|
||||
|
||||
### Code Review Checklist
|
||||
|
||||
```markdown
|
||||
## TDD Verification
|
||||
- [ ] New code has corresponding tests
|
||||
- [ ] Tests were written FIRST (check commit order)
|
||||
- [ ] Each test tests ONE behavior
|
||||
- [ ] Test names describe the scenario
|
||||
- [ ] No commented-out or skipped tests without reason
|
||||
- [ ] Coverage maintained or improved
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When TDD Is Not Appropriate
|
||||
|
||||
TDD may be skipped ONLY for:
|
||||
|
||||
### 1. Exploratory Prototypes
|
||||
|
||||
```python
|
||||
# prototype.py - Delete after learning
|
||||
# No tests needed for throwaway exploration
|
||||
def quick_test_api():
|
||||
response = requests.get("https://api.example.com")
|
||||
print(response.json())
|
||||
```
|
||||
|
||||
### 2. One-Time Scripts
|
||||
|
||||
```python
|
||||
# migrate_data.py - Run once, discard
|
||||
# Tests would cost more than value provided
|
||||
```
|
||||
|
||||
### 3. Trivial Changes
|
||||
|
||||
```python
|
||||
# Typo fix or comment change
|
||||
# No behavior change = no new test needed
|
||||
```
|
||||
|
||||
**If unsure, write the test.**
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference
|
||||
|
||||
| Phase | Rule | Check |
|
||||
|---------|-----------------------------------------|-------------------------------------|
|
||||
| Red | Write failing test first | Test fails for right reason |
|
||||
| Green | Write minimum code to pass | No extra features |
|
||||
| Refactor| Improve code while tests green | Run tests after each change |
|
||||
| Commit | Only commit green tests | All tests pass in CI |
|
||||
|
||||
## TDD Mantra
|
||||
|
||||
```
|
||||
Red. Green. Refactor. Commit. Repeat.
|
||||
|
||||
No test = No code.
|
||||
No green = No commit.
|
||||
No refactor = Technical debt.
|
||||
```
|
||||
Reference in New Issue
Block a user