Files
gh-withzombies-hyperpowers/skills/test-driven-development/resources/example-workflows.md
2025-11-30 09:06:38 +08:00

6.9 KiB

TDD Workflow Examples

This guide shows complete TDD workflows for common scenarios: bug fixes and feature additions.

Example: Bug Fix

Bug

Empty email is accepted when it should be rejected.

RED Phase: Write Failing Test

Swift:

func testRejectsEmptyEmail() async throws {
    let result = try await submitForm(FormData(email: ""))
    XCTAssertEqual(result.error, "Email required")
}

Verify RED: Watch It Fail

$ swift test --filter FormTests.testRejectsEmptyEmail
FAIL: XCTAssertEqual failed: ("nil") is not equal to ("Optional("Email required")")

Confirms:

  • Test fails (not errors)
  • Failure message shows email not being validated
  • Fails because feature missing (not typos)

GREEN Phase: Minimal Code

struct FormResult {
    var error: String?
}

func submitForm(_ data: FormData) async throws -> FormResult {
    if data.email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
        return FormResult(error: "Email required")
    }
    // ... rest of form processing
    return FormResult()
}

Verify GREEN: Watch It Pass

$ swift test --filter FormTests.testRejectsEmptyEmail
Test Case '-[FormTests testRejectsEmptyEmail]' passed

Confirms:

  • Test passes
  • Other tests still pass
  • No errors or warnings

REFACTOR: Clean Up

If multiple fields need validation:

extension FormData {
    func validate() -> String? {
        if email.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            return "Email required"
        }
        // Add other validations...
        return nil
    }
}

func submitForm(_ data: FormData) async throws -> FormResult {
    if let error = data.validate() {
        return FormResult(error: error)
    }
    // ... rest of form processing
    return FormResult()
}

Run tests again to confirm still green.


Example: Feature Addition

Feature

Calculate average of non-empty list.

RED Phase: Write Failing Test

TypeScript:

describe('average', () => {
  it('calculates average of non-empty list', () => {
    expect(average([1, 2, 3])).toBe(2);
  });
});

Verify RED: Watch It Fail

$ npm test -- --testNamePattern="average"
FAIL: ReferenceError: average is not defined

Confirms:

  • Function doesn't exist yet
  • Test would verify the behavior when function exists

GREEN Phase: Minimal Code

function average(numbers: number[]): number {
  const sum = numbers.reduce((acc, n) => acc + n, 0);
  return sum / numbers.length;
}

Verify GREEN: Watch It Pass

$ npm test -- --testNamePattern="average"
PASS: calculates average of non-empty list

Add Edge Case: Empty List

RED:

it('returns 0 for empty list', () => {
  expect(average([])).toBe(0);
});

Verify RED:

$ npm test -- --testNamePattern="average.*empty"
FAIL: Expected: 0, Received: NaN

GREEN:

function average(numbers: number[]): number {
  if (numbers.length === 0) return 0;
  const sum = numbers.reduce((acc, n) => acc + n, 0);
  return sum / numbers.length;
}

Verify GREEN:

$ npm test -- --testNamePattern="average"
PASS: 2 tests passed

REFACTOR: Clean Up

No duplication or unclear naming, so no refactoring needed. Move to next feature.


Example: Refactoring with Tests

Scenario

Existing function works but is hard to read. Tests exist and pass.

Current Code

fn process(data: Vec<i32>) -> i32 {
    let mut result = 0;
    for item in data {
        if item > 0 {
            result += item * 2;
        }
    }
    result
}

Existing Tests (Already Green)

#[test]
fn processes_positive_numbers() {
    assert_eq!(process(vec![1, 2, 3]), 12); // (1*2) + (2*2) + (3*2) = 12
}

#[test]
fn ignores_negative_numbers() {
    assert_eq!(process(vec![1, -2, 3]), 8); // (1*2) + (3*2) = 8
}

#[test]
fn handles_empty_list() {
    assert_eq!(process(vec![]), 0);
}

REFACTOR: Improve Clarity

fn process(data: Vec<i32>) -> i32 {
    data.iter()
        .filter(|&&n| n > 0)
        .map(|&n| n * 2)
        .sum()
}

Verify Still Green

$ cargo test
running 3 tests
test processes_positive_numbers ... ok
test ignores_negative_numbers ... ok
test handles_empty_list ... ok

test result: ok. 3 passed; 0 failed

Key: Tests prove refactoring didn't break behavior.


Common Patterns

Pattern: Adding Validation

  1. RED: Test that invalid input is rejected
  2. Verify RED: Confirm invalid input currently accepted
  3. GREEN: Add validation check
  4. Verify GREEN: Confirm validation works
  5. REFACTOR: Extract validation if reusable

Pattern: Adding Error Handling

  1. RED: Test that error condition is caught
  2. Verify RED: Confirm error currently unhandled
  3. GREEN: Add error handling
  4. Verify GREEN: Confirm error handled correctly
  5. REFACTOR: Consolidate error handling if duplicated

Pattern: Optimizing Performance

  1. Ensure tests exist and pass (if not, add tests first)
  2. REFACTOR: Optimize implementation
  3. Verify GREEN: Confirm tests still pass
  4. Measure: Confirm performance improved

Note: Never optimize without tests. You can't prove optimization didn't break behavior.


Workflow Checklist

For Each New Feature

  • Write one failing test
  • Run test, confirm it fails correctly
  • Write minimal code to pass
  • Run test, confirm it passes
  • Run all tests, confirm no regressions
  • Refactor if needed (staying green)
  • Commit

For Each Bug Fix

  • Write test reproducing the bug
  • Run test, confirm it fails (reproduces bug)
  • Fix the bug (minimal change)
  • Run test, confirm it passes (bug fixed)
  • Run all tests, confirm no regressions
  • Commit

For Each Refactoring

  • Confirm tests exist and pass
  • Make one small refactoring change
  • Run tests, confirm still green
  • Repeat until refactoring complete
  • Commit

Anti-Patterns to Avoid

Writing Multiple Tests Before Implementing

Why bad: You can't tell which test makes implementation fail. Write one, implement, repeat.

Changing Test to Make It Pass

Why bad: Test should define correct behavior. If test is wrong, fix test first, then re-run RED phase.

Adding Features Not Covered by Tests

Why bad: Untested code. If you need a feature, write test first.

Skipping RED Verification

Why bad: Test might pass immediately, meaning it doesn't test anything new.

Skipping GREEN Verification

Why bad: Test might fail for unexpected reason. Always verify expected pass.


Remember

  • One test at a time: Write test, implement, repeat
  • Watch it fail: Proves test actually tests something
  • Watch it pass: Proves implementation works
  • Stay green: All tests pass before moving on
  • Refactor freely: Tests catch breaks