TDD Enforcer
Enforce test-driven development. Write tests FIRST, then write the minimum code to make them pass.
The TDD Cycle
RED → GREEN → REFACTOR
1. RED: Write a failing test that defines the desired behavior
2. GREEN: Write the minimum code to make the test pass
3. REFACTOR: Clean up while keeping tests green
When to Activate
- Implementing a new feature
- Fixing a bug (write a failing test first that reproduces it)
- Refactoring existing code (ensure tests exist before touching it)
- User explicitly asks for tests or TDD
Core Rules
Rule 1: Test Before Code
Always write the test first. Never write implementation without a corresponding test.
User: "Add a function that validates email format"
❌ WRONG: Write validate_email() first, then add tests
✅ RIGHT:
1. Write test_validate_email_rejects_invalid() → fails
2. Write test_validate_email_accepts_valid() → fails
3. Write validate_email() → tests pass
4. Refactor if needed
Rule 2: One Behavior Per Test
Each test verifies exactly one behavior.
# ❌ Testing multiple things
def test_user_creation():
user = create_user("Alice", "alice@example.com")
assert user.name == "Alice"
assert user.email == "alice@example.com"
assert user.is_active == True
send_welcome_email(user) # Side effect!
assert user.welcome_sent == True
# ✅ Separate tests
def test_create_user_sets_name():
user = create_user("Alice", "alice@example.com")
assert user.name == "Alice"
def test_create_user_sets_email():
user = create_user("Alice", "alice@example.com")
assert user.email == "alice@example.com"
def test_create_user_is_active_by_default():
user = create_user("Alice", "alice@example.com")
assert user.is_active == True
Rule 3: AAA Pattern
Every test follows Arrange → Act → Assert.
// ✅ Clear AAA structure
test("should apply discount to orders over $100", () => {
// Arrange
const order = new Order();
order.addItem({ price: 150, quantity: 1 });
// Act
const total = order.calculateTotal();
// Assert
expect(total).toBe(135); // 10% discount applied
});
Rule 4: Test Names Describe Behavior
❌ test_user_1
❌ test_validation
❌ test_error
✅ test_create_user_rejects_empty_name
✅ test_validate_email_rejects_missing_at_symbol
✅ test_checkout_returns_400_when_cart_is_empty
Rule 5: Test Edge Cases
For every function, test:
- Happy path (normal expected input)
- Empty input (empty string, empty array, null)
- Boundary values (0, -1, max_int)
- Invalid input (wrong type, malformed data)
- Concurrent access (if applicable)
def test_parse_age():
assert parse_age("25") == 25 # happy path
assert parse_age("0") == 0 # boundary
assert parse_age("") is None # empty
assert parse_age("abc") is None # invalid
assert parse_age("-1") is None # negative
assert parse_age("999") == 999 # large
Test Structure Templates
Unit Test (Python / pytest)
import pytest
class TestFeatureName:
"""Tests for feature_name function."""
def test_expected_behavior_happy_path(self):
result = feature_name(valid_input)
assert result == expected_output
def test_edge_case_empty_input(self):
result = feature_name("")
assert result is None
def test_invalid_input_raises_error(self):
with pytest.raises(ValueError, match="specific error message"):
feature_name(invalid_input)
Unit Test (TypeScript / Vitest)
import { describe, it, expect } from "vitest";
describe("featureName", () => {
it("should return expected output for valid input", () => {
expect(featureName(validInput)).toBe(expectedOutput);
});
it("should return null for empty input", () => {
expect(featureName("")).toBeNull();
});
it("should throw for invalid input", () => {
expect(() => featureName(invalidInput)).toThrow("specific error");
});
});
Integration Test
def test_user_registration_flow():
"""Test the complete user registration process."""
# Arrange
client = TestClient(app)
payload = {"name": "Alice", "email": "alice@test.com", "password": "secure123"}
# Act
response = client.post("/api/register", json=payload)
# Assert
assert response.status_code == 201
data = response.json()
assert data["name"] == "Alice"
assert "password" not in data # Never expose password
# Verify side effect
user = db.query(User).filter_by(email="alice@test.com").first()
assert user is not None
assert bcrypt.checkpw("secure123", user.password_hash)
Bug Fix TDD Process
When fixing a bug, always follow this sequence:
1. REPRODUCE: Write a test that fails with the bug
2. CONFIRM: Run test, see it fail (RED)
3. FIX: Write minimum code to make test pass
4. VERIFY: Run test, see it pass (GREEN)
5. REGRESSION: Run full test suite, ensure nothing broke
❌ WRONG: "I found the bug, let me fix it" (fixes without test)
✅ RIGHT: "I'll write a test that reproduces this bug first"
Coverage Guidelines
Target coverage by code type:
- Business logic: 90-100%
- API endpoints: 80-90%
- Utility functions: 90-100%
- UI components: 60-80%
- Configuration: Skip (test via integration)
- Glue code: Skip (covered by integration tests)
Do NOT chase 100% coverage. Focus on meaningful tests for critical paths.
Anti-Patterns
Testing Implementation Details
❌ Testing internal state: expect(component.state.counter).toBe(1)
✅ Testing behavior: expect(screen.getByText("Count: 1")).toBeInTheDocument()
Mocking Everything
❌ Mock database, mock API, mock filesystem, mock time
✅ Use real objects where possible, mock only external services
Brittle Assertions
❌ expect(result).toBe("exactly this string with no flexibility")
✅ expect(result).toContain("key phrase")
✅ expect(result).toMatch(/^\d{4}-\d{2}-\d{2}$/) // date pattern
Test Interdependence
❌ test_b depends on test_a running first
✅ Each test is fully independent, can run in any order
Quick Reference
| Situation | Action |
|---|---|
| New feature | Write failing test first |
| Bug fix | Write reproducing test first |
| Refactor | Ensure tests exist first |
| No tests exist | Write tests before changing code |
| Test is flaky | Fix or delete it, never ignore it |