From 1f1eabd1ed11295c992d2941579d9bcf1b8609ba Mon Sep 17 00:00:00 2001 From: m3tm3re Date: Wed, 18 Feb 2026 17:30:20 +0100 Subject: [PATCH] feat(rules): add strict TDD enforcement ruleset with AI patterns --- rules/concerns/tdd.md | 476 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 476 insertions(+) create mode 100644 rules/concerns/tdd.md diff --git a/rules/concerns/tdd.md b/rules/concerns/tdd.md new file mode 100644 index 0000000..8f98fbf --- /dev/null +++ b/rules/concerns/tdd.md @@ -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. +```