playbook/outfitter-agents/plugins/outfitter/skills/tdd/examples/bug-fix.md

9.2 KiB

TDD Session: Bug Fix

TDD workflow for fixing a production bug with test reproduction first.

Bug Report

Issue: Division by zero crashes calculator service Reporter: User reported 500 error when calculating percentage with zero total Stack Trace:

Error: Division by zero
  at calculatePercentage (calculator.ts:45)
  at handler (api/stats.ts:23)

Priority: High — causes service crash

TDD Workflow

Task Setup

1. Write failing test reproducing bug [in_progress]
2. Fix bug with minimal code [pending]
3. Refactor if needed [pending]
4. Verify fix [pending]

RED Phase: Reproduce Bug (4 min)

Confidence: ▓▓░░░ → Can reproduce, know the fix

First, understand the current implementation:

Read: src/calculator/percentage.ts

export function calculatePercentage(value: number, total: number): number {
  return (value / total) * 100
}

Issue clear: No validation for zero total.

Write failing test: src/calculator/percentage.test.ts

import { describe, test, expect } from 'bun:test'
import { calculatePercentage } from './percentage'

describe('calculatePercentage', () => {
  // Existing tests
  test('calculates percentage correctly', () => {
    expect(calculatePercentage(25, 100)).toBe(25)
    expect(calculatePercentage(50, 200)).toBe(25)
  })

  // NEW: Bug reproduction test
  test('handles division by zero gracefully', () => {
    const result = calculatePercentage(10, 0)

    expect(result).toBe(0)
    // OR throw error approach:
    // expect(() => calculatePercentage(10, 0)).toThrow('Total cannot be zero')
  })

  test('handles zero value with valid total', () => {
    expect(calculatePercentage(0, 100)).toBe(0)
  })
})

Run test: bun test percentage.test.ts

calculatePercentage
  ✓ calculates percentage correctly
  ✗ handles division by zero gracefully
    Expected: 0
    Received: Infinity
  ✓ handles zero value with valid total

1 failing

Bug reproduced successfully — test fails as expected.

Commit: test: add failing test for division by zero bug

Task Update:

1. Write failing test reproducing bug [completed]
2. Fix bug with minimal code [in_progress]
3. Refactor if needed [pending]
4. Verify fix [pending]

GREEN Phase: Fix Bug (3 min)

Confidence: ▓▓▓▓░ → Know exact fix needed

Update: src/calculator/percentage.ts

export function calculatePercentage(value: number, total: number): number {
  // Fix: Handle division by zero
  if (total === 0) {
    return 0
  }

  return (value / total) * 100
}

Run tests: bun test percentage.test.ts

calculatePercentage
  ✓ calculates percentage correctly
  ✓ handles division by zero gracefully
  ✓ handles zero value with valid total

3 passing

All tests pass — bug fixed.

Verify no regressions: bun test

All test suites passing (23 tests total)

No other tests broken — safe fix.

Commit: fix: handle division by zero in percentage calculation

Task Update:

1. Write failing test reproducing bug [completed]
2. Fix bug with minimal code [completed]
3. Refactor if needed [in_progress]
4. Verify fix [pending]

REFACTOR Phase: Consider Improvements (5 min)

Current fix works but could be more robust. Consider:

  1. Should negative totals be handled?
  2. Should we return error instead of 0?
  3. Type safety improvements?

Decision: Add input validation, use Result type for better error handling.

Refactored: src/calculator/percentage.ts

type PercentageResult =
  | { type: 'success'; value: number }
  | { type: 'error'; code: 'ZERO_TOTAL' | 'NEGATIVE_TOTAL' }

export function calculatePercentage(
  value: number,
  total: number
): PercentageResult {
  // Validate total
  if (total === 0) {
    return { type: 'error', code: 'ZERO_TOTAL' }
  }

  if (total < 0) {
    return { type: 'error', code: 'NEGATIVE_TOTAL' }
  }

  return {
    type: 'success',
    value: (value / total) * 100,
  }
}

Update tests to match new signature:

describe('calculatePercentage', () => {
  test('calculates percentage correctly', () => {
    const result1 = calculatePercentage(25, 100)
    const result2 = calculatePercentage(50, 200)

    expect(result1).toEqual({ type: 'success', value: 25 })
    expect(result2).toEqual({ type: 'success', value: 25 })
  })

  test('returns error for division by zero', () => {
    const result = calculatePercentage(10, 0)

    expect(result).toEqual({
      type: 'error',
      code: 'ZERO_TOTAL',
    })
  })

  test('returns error for negative total', () => {
    const result = calculatePercentage(10, -100)

    expect(result).toEqual({
      type: 'error',
      code: 'NEGATIVE_TOTAL',
    })
  })

  test('handles zero value with valid total', () => {
    const result = calculatePercentage(0, 100)

    expect(result).toEqual({ type: 'success', value: 0 })
  })
})

Run tests: bun test percentage.test.ts

calculatePercentage
  ✓ calculates percentage correctly
  ✓ returns error for division by zero
  ✓ returns error for negative total
  ✓ handles zero value with valid total

4 passing

Update usage in api/stats.ts:

// Before
const percentage = calculatePercentage(completed, total)
return { percentage }

// After
const result = calculatePercentage(completed, total)
if (result.type === 'error') {
  return { error: 'Invalid calculation', code: result.code }
}
return { percentage: result.value }

Run all tests: bun test

All tests passing (24 tests total)

No regressions — refactor successful.

Commit: refactor: use Result type for percentage calculation errors

Task Update:

1. Write failing test reproducing bug [completed]
2. Fix bug with minimal code [completed]
3. Refactor if needed [completed]
4. Verify fix [in_progress]

VERIFY Phase: Comprehensive Validation (4 min)

Run full test suite with coverage:

bun test --coverage
Coverage Summary:
  percentage.ts: 100% statements, 100% branches

All tests passing (24 total)

Run mutation testing on updated code:

bun x stryker run --mutate "src/calculator/percentage.ts"
Mutation Score: 100%
All mutants killed

Mutations tested:
  ✓ === → !== (killed by "returns error for division by zero")
  ✓ < → <= (killed by "returns error for negative total")
  ✓ / → * (killed by "calculates percentage correctly")
  ✓ 0 → 1 (killed by "handles zero value with valid total")

Perfect mutation score — tests thoroughly verify behavior.

Integration test to verify API fix:

describe('GET /api/stats', () => {
  test('handles zero total gracefully', async () => {
    const response = await request(app)
      .get('/api/stats')
      .query({ completed: 10, total: 0 })

    expect(response.status).toBe(200)
    expect(response.body).toMatchObject({
      error: 'Invalid calculation',
      code: 'ZERO_TOTAL',
    })
  })
})

Run integration tests: bun test tests/integration/

API Integration Tests
  ✓ handles zero total gracefully

All integration tests passing

Task Update:

1. Write failing test reproducing bug [completed]
2. Fix bug with minimal code [completed]
3. Refactor if needed [completed]
4. Verify fix [completed]

Session Summary

Duration: 16 minutes total

  • RED: 4 min
  • GREEN: 3 min
  • REFACTOR: 5 min
  • VERIFY: 4 min

Bug: Division by zero crash Fix: Added validation with Result type Tests: 4 new tests + 1 integration test Coverage: 100% on changed code Mutation Score: 100%

Improvements beyond minimal fix:

  • Used discriminated union for error handling
  • Added negative total validation
  • Updated API to handle error results
  • Added integration test

Production deployment:

  • All tests passing
  • No regressions detected
  • Error handling verified
  • Ready to deploy

Key TDD Bug Fix Principles

  1. RED first: Always reproduce bug with failing test before fixing
  2. Minimal GREEN: Fix the immediate issue first
  3. Refactor for robustness: Improve error handling and edge cases
  4. Verify thoroughly: Run full suite + mutation tests + integration tests
  5. Document in test: Test name describes the bug being fixed

Anti-patterns Avoided

Avoided jumping straight to fix without test:

// ❌ Wrong approach
// 1. See bug report
// 2. Add if (total === 0) return 0
// 3. Deploy and hope

// ✓ Correct TDD approach
// 1. Write failing test reproducing bug
// 2. Verify test fails
// 3. Add minimal fix
// 4. Verify test passes
// 5. Refactor for robustness
// 6. Verify with mutation testing

Avoided over-engineering initial fix:

// ❌ Too complex for first fix
if (total === 0 || total < 0 || !isFinite(total) || isNaN(total)) {
  throw new ValidationError(...)
}

// ✓ Minimal fix first (GREEN phase)
if (total === 0) {
  return 0
}

// ✓ Then refactor with proper error handling (REFACTOR phase)
if (total === 0) {
  return { type: 'error', code: 'ZERO_TOTAL' }
}

Commit History

test: add failing test for division by zero bug
fix: handle division by zero in percentage calculation
refactor: use Result type for percentage calculation errors

Clean, focused commits showing TDD progression.