Claude
Skills
Sign in
Back

test-driven-development

Included with Lifetime
$97 forever

Use when writing production code. Enforces RED-GREEN-REFACTOR cycle: write failing test, make it pass, improve design. Prevents test-after development and ensures verified behavior.

Design

What this skill does


# Test-Driven Development (TDD)

**Iron Law:** "NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST"

## When to Use

Use this skill when:
- Writing new functions, methods, or classes
- Adding features to existing code
- Fixing bugs (write test that reproduces bug first)
- Refactoring code (tests verify behavior preservation)
- Implementing API endpoints or business logic
- Creating UI components with testable behavior

## Red Flags (Violation Indicators)

Watch for these patterns that indicate TDD violations:

- [ ] **Implementation First** - Writing production code before any test exists ("I'll write the function, then add tests")
- [ ] **"Tests After" Promise** - Planning to write tests later ("Let me implement this quickly, I'll add tests after")
- [ ] **"Same Purpose" Rationalization** - Claiming manual testing is equivalent ("I tested it manually, that's the same thing")
- [ ] **Skipping "Simple" Code** - Avoiding tests for "obvious" logic ("This function is too simple to test")
- [ ] **Happy Path Only** - Writing tests only for success cases, ignoring errors ("The normal case works, that's enough")
- [ ] **No Test for Changes** - Modifying code without adding corresponding test ("Just a small change, doesn't need a test")
- [ ] **"Just a Small Fix"** - Bypassing TDD for "quick fixes" ("It's only one line, I don't need a test")

**Violation Detection:** If you find yourself saying "I'll test this after I get it working," you're violating TDD.

## RED-GREEN-REFACTOR Cycle

TDD follows a strict 3-phase workflow:

### Phase 1: RED (Write a Failing Test)

**Objective:** Specify desired behavior through a test that fails

**Steps:**
1. **Write the test first** - Before any production code exists
2. **Define expected behavior** - What should the code do?
3. **Use API you wish existed** - Test the interface you want
4. **Run the test** - Verify it fails (if it passes, you didn't test new behavior)
5. **Check failure reason** - Ensure it fails for the right reason (missing code, not syntax error)

**Example (TypeScript/Jest):**
```typescript
// tests/user-validator.test.ts
describe('UserValidator', () => {
  it('rejects email without @ symbol', () => {
    const validator = new UserValidator();
    const result = validator.validateEmail('invalid-email');
    expect(result.isValid).toBe(false);
    expect(result.error).toBe('Email must contain @ symbol');
  });
});

// Run: npm test
// Result: FAIL - UserValidator is not defined ✓ (correct failure)
```

**Red Phase Complete When:** Test fails with expected error message

---

### Phase 2: GREEN (Make the Test Pass)

**Objective:** Write minimal code to make test pass

**Steps:**
1. **Write minimal code** - Just enough to pass the test
2. **Don't optimize yet** - Resist the urge to add "nice-to-have" features
3. **Run the test** - Verify it passes
4. **Commit frequently** - Small, passing test = commit point

**Example (TypeScript):**
```typescript
// src/user-validator.ts
interface ValidationResult {
  isValid: boolean;
  error?: string;
}

export class UserValidator {
  validateEmail(email: string): ValidationResult {
    if (!email.includes('@')) {
      return { isValid: false, error: 'Email must contain @ symbol' };
    }
    return { isValid: true };
  }
}

// Run: npm test
// Result: PASS ✓
```

**Green Phase Complete When:** Test passes consistently

---

### Phase 3: REFACTOR (Improve Design)

**Objective:** Improve code quality while keeping tests green

**Steps:**
1. **Look for duplication** - Extract repeated code
2. **Improve naming** - Make intent clearer
3. **Simplify logic** - Reduce complexity
4. **Run tests after each change** - Ensure behavior preserved
5. **Commit when satisfied** - Refactored code + passing tests = commit

**Example (TypeScript - Refactored):**
```typescript
// src/user-validator.ts
interface ValidationResult {
  isValid: boolean;
  error?: string;
}

export class UserValidator {
  private static readonly EMAIL_REQUIRED_CHARS = '@';
  private static readonly EMAIL_ERROR = 'Email must contain @ symbol';

  validateEmail(email: string): ValidationResult {
    if (!this.containsRequiredChars(email)) {
      return this.createError(UserValidator.EMAIL_ERROR);
    }
    return this.createSuccess();
  }

  private containsRequiredChars(email: string): boolean {
    return email.includes(UserValidator.EMAIL_REQUIRED_CHARS);
  }

  private createError(message: string): ValidationResult {
    return { isValid: false, error: message };
  }

  private createSuccess(): ValidationResult {
    return { isValid: true };
  }
}

// Run: npm test
// Result: PASS ✓ (behavior unchanged)
```

**Refactor Phase Complete When:** Code is clean AND tests still pass

---

## Anti-patterns Table

| Anti-pattern | ✗ Wrong Approach | ✓ Correct TDD Approach |
|--------------|------------------|------------------------|
| **Test-After** | Write `calculateTotal()` function, then write tests | Write test for `calculateTotal()`, see it fail, implement function |
| **Empty Tests** | Write test that always passes: `expect(true).toBe(true)` | Write test that fails until production code is correct |
| **Happy-Path-Only** | Test only valid inputs: `validateEmail('[email protected]')` | Test invalid inputs too: `validateEmail('no-at-sign')`, `validateEmail('')` |
| **Skip-Simple** | Skip test for "obvious" `add(a, b) { return a + b }` | Write test: `expect(add(2, 3)).toBe(5)` - bugs hide in "simple" code |
| **Change-Then-Test** | Modify `calculateDiscount()`, run app manually, then add test | Write failing test showing bug, modify code until test passes |

## Testing Strategy by Code Type

### Pure Functions

**Pattern:** 1 happy path + 3 edge cases minimum

**Example (TypeScript):**
```typescript
describe('calculateDiscount', () => {
  it('applies 10% discount to $100 purchase', () => {
    expect(calculateDiscount(100, 0.1)).toBe(90);
  });

  it('returns 0 for negative amounts', () => {
    expect(calculateDiscount(-50, 0.1)).toBe(0);
  });

  it('returns original amount for 0 discount', () => {
    expect(calculateDiscount(100, 0)).toBe(100);
  });

  it('throws error for discount > 1', () => {
    expect(() => calculateDiscount(100, 1.5)).toThrow('Discount must be <= 1');
  });
});
```

---

### Error Handlers

**Pattern:** 1 test per error type + 1 success case

**Example (Python/pytest):**
```python
def test_divide_by_zero_raises_error():
    with pytest.raises(ZeroDivisionError, match="Cannot divide by zero"):
        divide(10, 0)

def test_divide_non_numeric_raises_error():
    with pytest.raises(TypeError, match="Arguments must be numbers"):
        divide("10", 5)

def test_divide_returns_float():
    result = divide(10, 3)
    assert result == pytest.approx(3.333, rel=1e-3)
```

---

### UI Components

**Pattern:** 1 render test + 3 interaction tests

**Example (TypeScript/React Testing Library):**
```typescript
describe('LoginForm', () => {
  it('renders email and password inputs', () => {
    render(<LoginForm />);
    expect(screen.getByLabelText('Email')).toBeInTheDocument();
    expect(screen.getByLabelText('Password')).toBeInTheDocument();
  });

  it('shows error for invalid email', async () => {
    render(<LoginForm />);
    await userEvent.type(screen.getByLabelText('Email'), 'invalid');
    await userEvent.click(screen.getByRole('button', { name: 'Login' }));
    expect(screen.getByText('Invalid email address')).toBeInTheDocument();
  });

  it('disables submit button while loading', async () => {
    render(<LoginForm onSubmit={async () => await delay(1000)} />);
    const submitButton = screen.getByRole('button', { name: 'Login' });
    await userEvent.click(submitButton);
    expect(submitButton).toBeDisabled();
  });

  it('calls onSubmit with form data', async () => {
    const onSubmit = jest.fn();
    render(<LoginForm onSubmit={onSubmit} />);
    await userEvent.type(screen.getByLabelText('Email'), '[email protected]');
    await userEvent.type(screen.getByLabelText('Password')

Related in Design