Files
gh-yaleh-meta-cc-claude/skills/code-refactoring/templates/tdd-refactoring-workflow.md
2025-11-30 09:07:22 +08:00

13 KiB
Raw Permalink Blame History

TDD Refactoring Workflow

Purpose: Enforce test-driven discipline during refactoring to ensure behavior preservation and quality

When to Use: During ALL refactoring work

Origin: Iteration 1 - Problem E1 (No TDD Enforcement)


TDD Principle for Refactoring

Red-Green-Refactor Cycle (adapted for refactoring existing code):

  1. Green (Baseline): Ensure existing tests pass
  2. Red (Add Tests): Write tests for uncovered behavior (tests should pass immediately since code exists)
  3. Refactor: Restructure code while maintaining green tests
  4. Green (Verify): Confirm all tests still pass after refactoring

Key Difference from New Development TDD:

  • New Development: Write failing test → Make it pass → Refactor
  • Refactoring: Ensure passing tests → Add missing tests (passing) → Refactor → Keep tests passing

Workflow Steps

Phase 1: Baseline Green (Ensure Safety Net)

Goal: Verify existing tests provide safety net for refactoring

Step 1: Run Existing Tests

go test -v ./internal/query/... > tests-baseline.txt

Checklist:

  • All existing tests pass: YES / NO
  • Test count: ___ tests
  • Duration: ___s
  • If any fail: FIX BEFORE PROCEEDING

Step 2: Check Coverage

go test -cover ./internal/query/...
go test -coverprofile=coverage.out ./internal/query/...
go tool cover -html=coverage.out -o coverage.html

Checklist:

  • Overall coverage: ___%
  • Target function coverage: ___%
  • Uncovered lines identified: YES / NO
  • Coverage file: coverage.html (review in browser)

Step 3: Identify Coverage Gaps

Review coverage.html and identify:

  • Uncovered branches: _______________
  • Uncovered error paths: _______________
  • Uncovered edge cases: _______________
  • Missing edge case examples:
    • Empty inputs: ___ (e.g., empty slice, nil, zero)
    • Boundary conditions: ___ (e.g., single element, max value)
    • Error conditions: ___ (e.g., invalid input, out of range)

Decision Point:

  • If coverage ≥95% on target code: Proceed to Phase 2 (Refactor)
  • If coverage <95%: Proceed to Phase 1b (Write Missing Tests)

Phase 1b: Write Missing Tests (Red → Immediate Green)

Goal: Add tests for uncovered code paths BEFORE refactoring

For Each Coverage Gap:

1. Write Characterization Test (documents current behavior):

func TestCalculateSequenceTimeSpan_<EdgeCase>(t *testing.T) {
    // Setup: Create input that triggers uncovered path
    // ...

    // Execute: Call function
    result := calculateSequenceTimeSpan(occurrences, entries, toolCalls)

    // Verify: Document current behavior (even if it's wrong)
    assert.Equal(t, <expected>, result, "current behavior")
}

Test Naming Convention:

  • Test<FunctionName>_<EdgeCase> (e.g., TestCalculateTimeSpan_EmptyOccurrences)
  • Test<FunctionName>_<Scenario> (e.g., TestCalculateTimeSpan_SingleOccurrence)

2. Verify Test Passes (should pass immediately since code exists):

go test -v -run Test<FunctionName>_<EdgeCase> ./...

Checklist:

  • Test written: Test<FunctionName>_<EdgeCase>
  • Test passes immediately: YES / NO
  • If NO: Bug in test or unexpected current behavior → Fix test
  • Coverage increased: __% → ___%

3. Commit Test:

git add <test_file>
git commit -m "test: add <edge-case> test for <function>"

Repeat for all coverage gaps until target coverage ≥95%

Coverage Target

  • Overall coverage: ≥85% (project minimum)
  • Target function coverage: ≥95% (refactoring requirement)
  • New test coverage: ≥100% (all new tests pass)

Checkpoint: Before proceeding to refactoring:

  • All tests pass: PASS
  • Target function coverage: ≥95%
  • Coverage gaps documented if <95%: _______________

Phase 2: Refactor (Maintain Green)

Goal: Restructure code while keeping all tests passing

For Each Refactoring Step:

1. Plan Single Refactoring Transformation:

  • Transformation type: _______________ (Extract Method, Inline, Rename, etc.)
  • Target code: _______________ (function, lines, scope)
  • Expected outcome: _______________ (complexity reduction, clarity, etc.)
  • Estimated time: ___ minutes

2. Make Minimal Change:

Examples:

  • Extract Method: Move lines X-Y to new function <name>
  • Simplify Conditional: Replace nested if with guard clause
  • Rename: Change <oldName> to <newName>

Checklist:

  • Single, focused change: YES / NO
  • No behavioral changes: Only structural / organizational
  • Files modified: _______________
  • Lines changed: ~___

3. Run Tests Immediately:

go test -v ./internal/query/... | tee test-results-step-N.txt

Checklist:

  • All tests pass: PASS / FAIL
  • Duration: ___s (should be quick, <10s)
  • If FAIL: ROLLBACK IMMEDIATELY

4. Verify Coverage Maintained:

go test -cover ./internal/query/...

Checklist:

  • Coverage: Before __% → After ___%
  • Change: +/- ___%
  • If decreased >1%: Investigate (might need to update tests)
  • If decreased >5%: ROLLBACK

5. Verify Complexity:

gocyclo -over 10 internal/query/<target-file>.go

Checklist:

  • Target function complexity: ___
  • Change from previous: +/- ___
  • If increased: Not a valid refactoring step → ROLLBACK

6. Commit Incremental Progress:

git add .
git commit -m "refactor(<file>): <pattern> - <what changed>"

Example Commit Messages:

  • refactor(sequences): extract collectTimestamps helper
  • refactor(sequences): simplify min/max calculation
  • refactor(sequences): rename ts to timestamp for clarity

Checklist:

  • Commit hash: _______________
  • Message follows convention: YES / NO
  • Commit is small, focused: YES / NO

Repeat refactoring steps until refactoring complete or target achieved


Phase 3: Final Verification (Confirm Green)

Goal: Comprehensive verification that refactoring succeeded

1. Run Full Test Suite

go test -v ./... | tee test-results-final.txt

Checklist:

  • All tests pass: PASS / FAIL
  • Test count: ___ (should match baseline or increase)
  • Duration: ___s
  • No flaky tests: All consistent

2. Verify Coverage Improved or Maintained

go test -cover ./internal/query/...
go test -coverprofile=coverage-final.out ./internal/query/...
go tool cover -func=coverage-final.out | grep total

Checklist:

  • Baseline coverage: ___%
  • Final coverage: ___%
  • Change: +___%
  • Target met (≥85% overall, ≥95% refactored code): YES / NO

3. Compare Baseline and Final Metrics

Metric Baseline Final Change Target Met
Complexity ___ ___ ___% YES / NO
Coverage ___% ___% +___% YES / NO
Test count ___ ___ +___ N/A
Test duration ___s ___s ___s N/A

Checklist:

  • All targets met: YES / NO
  • If NO: Document gaps and plan next iteration

4. Update Documentation

# Add/update GoDoc comments for refactored code
# Example:
// calculateSequenceTimeSpan calculates the time span in minutes between
// the first and last occurrence of a sequence pattern across turns.
// Returns 0 if no valid timestamps found.

Checklist:

  • GoDoc added/updated: YES / NO
  • Public functions documented: ___ / ___ (100%)
  • Parameter descriptions clear: YES / NO
  • Return value documented: YES / NO

TDD Metrics (Track Over Time)

Refactoring Session: ___ (e.g., calculateSequenceTimeSpan - 2025-10-19)

Metric Value
Baseline coverage ___%
Final coverage ___%
Coverage improvement +___%
Tests added ___
Test failures during refactoring ___
Rollbacks due to test failures ___
Time spent writing tests ___ min
Time spent refactoring ___ min
Test writing : Refactoring ratio ___:1

TDD Discipline Score: (Tests passing after each step) / (Total steps) × 100% = ___%

Target: 100% TDD discipline (tests pass after EVERY step)


Common TDD Refactoring Patterns

Pattern 1: Extract Method with Tests

Scenario: Function too complex, need to extract helper

Steps:

  1. Ensure tests pass
  2. Write test for behavior to be extracted (if not covered)
  3. Extract method
  4. Tests still pass
  5. Write direct test for new extracted method
  6. Tests pass
  7. Commit

Example:

// Before:
func calculate() {
    // ... 20 lines of timestamp collection
    // ... 15 lines of min/max finding
}

// After:
func calculate() {
    timestamps := collectTimestamps()
    return findMinMax(timestamps)
}

func collectTimestamps() []int64 { /* extracted */ }
func findMinMax([]int64) int { /* extracted */ }

Tests:

  • Existing: TestCalculate (still passes)
  • New: TestCollectTimestamps (covers extracted logic)
  • New: TestFindMinMax (covers min/max logic)

Pattern 2: Simplify Conditionals with Tests

Scenario: Nested conditionals hard to read, need to simplify

Steps:

  1. Ensure tests pass (covering all branches)
  2. If branches uncovered: Add tests for all paths
  3. Simplify conditionals (guard clauses, early returns)
  4. Tests still pass
  5. Commit

Example:

// Before: Nested conditionals
if len(timestamps) > 0 {
    minTs := timestamps[0]
    maxTs := timestamps[0]
    for _, ts := range timestamps[1:] {
        if ts < minTs {
            minTs = ts
        }
        if ts > maxTs {
            maxTs = ts
        }
    }
    return int((maxTs - minTs) / 60)
} else {
    return 0
}

// After: Guard clause
if len(timestamps) == 0 {
    return 0
}
minTs := timestamps[0]
maxTs := timestamps[0]
for _, ts := range timestamps[1:] {
    if ts < minTs {
        minTs = ts
    }
    if ts > maxTs {
        maxTs = ts
    }
}
return int((maxTs - minTs) / 60)

Tests: No new tests needed (behavior unchanged), existing tests verify correctness


Pattern 3: Remove Duplication with Tests

Scenario: Duplicated code blocks, need to extract to shared helper

Steps:

  1. Ensure tests pass
  2. Identify duplication: Lines X-Y in File A same as Lines M-N in File B
  3. Extract to shared helper
  4. Replace first occurrence with helper call
  5. Tests pass
  6. Replace second occurrence
  7. Tests pass
  8. Commit

Example:

// Before: Duplication
// File A:
if startTs > 0 {
    timestamps = append(timestamps, startTs)
}

// File B:
if endTs > 0 {
    timestamps = append(timestamps, endTs)
}

// After: Shared helper
func appendIfValid(timestamps []int64, ts int64) []int64 {
    if ts > 0 {
        return append(timestamps, ts)
    }
    return timestamps
}

// File A: timestamps = appendIfValid(timestamps, startTs)
// File B: timestamps = appendIfValid(timestamps, endTs)

Tests:

  • Existing tests for Files A and B (still pass)
  • New: TestAppendIfValid (covers helper)

TDD Anti-Patterns (Avoid These)

Anti-Pattern 1: "Skip Tests, Code Seems Fine"

Problem: Refactor without running tests Risk: Break behavior without noticing Fix: ALWAYS run tests after each change

Anti-Pattern 2: "Write Tests After Refactoring"

Problem: Tests written to match new code (not verify behavior) Risk: Tests pass but behavior changed Fix: Write tests BEFORE refactoring (characterization tests)

Anti-Pattern 3: "Batch Multiple Changes Before Testing"

Problem: Make 3-4 changes, then run tests Risk: If tests fail, hard to identify which change broke it Fix: Test after EACH change

Anti-Pattern 4: "Update Tests to Match New Code"

Problem: Tests fail after refactoring, so "fix" tests Risk: Masking behavioral changes Fix: If tests fail, rollback refactoring → Fix code, not tests

Anti-Pattern 5: "Low Coverage is OK for Refactoring"

Problem: Refactor code with <75% coverage Risk: Behavioral changes not caught by tests Fix: Achieve ≥95% coverage BEFORE refactoring


Automation Support

Continuous Testing (automatically run tests on file save):

Option 1: File Watcher (entr)

# Install entr
go install github.com/eradman/entr@latest

# Auto-run tests on file change
find internal/query -name '*.go' | entr -c go test ./internal/query/...

Option 2: IDE Integration

  • VS Code: Go extension auto-runs tests on save
  • GoLand: Configure test auto-run in settings
  • Vim: Use vim-go with :GoTestFunc on save

Option 3: Pre-Commit Hook

# .git/hooks/pre-commit
#!/bin/bash
go test ./... || exit 1
go test -cover ./... | grep -E 'coverage: [0-9]+' || exit 1

Checklist:

  • Automation setup: YES / NO
  • Tests run automatically: YES / NO
  • Feedback time: ___s (target <5s)

Notes

  • TDD Discipline: Tests must pass after EVERY single change
  • Small Steps: Each refactoring step should take <10 minutes
  • Fast Tests: Test suite should run in <10 seconds for fast feedback
  • No Guessing: If unsure about behavior, write test to document it
  • Coverage Goal: ≥95% for code being refactored, ≥85% overall

Version: 1.0 (Iteration 1) Next Review: Iteration 2 (refine based on usage data) Automation: See Problem V1 for automated complexity checking integration