home / skills / rshankras / claude-code-apple-skills / tdd-refactor-guard

tdd-refactor-guard skill

/skills/testing/tdd-refactor-guard

This skill prevents risky refactors by verifying test coverage and generating characterization tests before AI modifies Swift code.

npx playbooks add skill rshankras/claude-code-apple-skills --skill tdd-refactor-guard

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
7.0 KB
---
name: tdd-refactor-guard
description: Pre-refactor safety checklist. Verifies test coverage exists before AI modifies existing code. Use before asking AI to refactor anything.
allowed-tools: [Read, Write, Edit, Glob, Grep, Bash, AskUserQuestion]
---

# TDD Refactor Guard

A safety gate that runs before any AI-assisted refactoring. Ensures tests exist to catch regressions. If coverage is insufficient, generates characterization tests first.

## When This Skill Activates

Use this skill when the user:
- Says "refactor this" or "clean this up"
- Wants AI to "restructure" or "reorganize" code
- Asks to "extract", "inline", "rename", or "simplify" existing code
- Plans to migrate between patterns (e.g., MVC → MVVM, CoreData → SwiftData)
- Wants AI to "improve" or "modernize" existing code

## Why This Guard Exists

```
Without guard:               With guard:
"Claude, refactor this"      "Claude, refactor this"
  → AI rewrites code           → Guard checks for tests
  → Something breaks           → Tests missing → generate them
  → You don't know what        → Tests pass → proceed with refactor
  → Debugging nightmare         → Tests fail → exact regression found
```

## Process

### Step 1: Identify Scope

What code is being refactored?

```
Ask user: "Which files/classes are you planning to refactor?"
```

Or detect from context:
```
Glob: [files mentioned in conversation]
Read: each file to understand scope
```

Determine:
- [ ] Files involved
- [ ] Public API surface (methods, properties)
- [ ] Dependencies (what calls this code, what it calls)
- [ ] Side effects (network, disk, notifications, UI updates)

### Step 2: Check Existing Test Coverage

```
Grep: "class.*Tests.*XCTestCase|@Suite.*Tests|struct.*Tests" in test targets
Grep: "@Test|func test" that reference the classes being refactored
```

#### Coverage Assessment

| Level | Criteria | Action |
|-------|----------|--------|
| Good (> 80%) | Most public methods have tests for happy + error paths | Proceed with refactoring |
| Partial (40-80%) | Some methods tested, missing edge cases | Add characterization tests for uncovered methods |
| Minimal (< 40%) | Few or no tests | Generate full characterization test suite first |
| None (0%) | No test file exists | STOP — create characterization tests before any refactoring |

### Step 3: Evaluate Test Quality

Even if tests exist, check quality:

```swift
// ❌ Low-quality test — doesn't actually verify behavior
@Test("test items")
func testItems() {
    let vm = ViewModel()
    vm.loadItems()
    #expect(true)  // Always passes, tests nothing
}

// ❌ Tests implementation, not behavior
@Test("calls repository")
func testCallsRepo() {
    let vm = ViewModel()
    vm.loadItems()
    #expect(mockRepo.fetchCallCount == 1)  // Brittle
}

// ✅ Tests observable behavior
@Test("loads items into published array")
func testLoadsItems() async {
    let vm = ViewModel(repo: MockRepo(items: [.sample]))
    await vm.loadItems()
    #expect(vm.items.count == 1)
    #expect(vm.items.first?.title == "Sample")
}
```

**Quality checklist:**
- [ ] Tests verify **outputs/state**, not implementation details
- [ ] Both happy path and error paths covered
- [ ] Edge cases (empty, nil, boundary) tested
- [ ] Async behavior properly awaited
- [ ] No `#expect(true)` or always-passing assertions

### Step 4: Generate Missing Tests

If coverage is insufficient, use `characterization-test-generator/`:

```markdown
Before refactoring [ClassName], generate characterization tests:
1. Read the source file
2. Identify all public methods
3. Generate tests that capture CURRENT behavior
4. Run and verify all pass
```

### Step 5: Green Light / Red Light

#### Green Light — Proceed

```markdown
## Refactor Guard: ✅ PROCEED

**Files**: ItemManager.swift, ItemRepository.swift
**Test coverage**: Good (12 tests covering 8/9 public methods)
**Test quality**: Adequate — tests verify behavior, not implementation

Safe to refactor. Run tests after each change:
`xcodebuild test -scheme YourApp`
```

#### Yellow Light — Needs Work

```markdown
## Refactor Guard: ⚠️ NEEDS TESTS

**Files**: ItemManager.swift, ItemRepository.swift
**Test coverage**: Partial (5 tests covering 4/9 public methods)
**Missing coverage**:
- `ItemManager.sort()` — no test
- `ItemManager.filter(by:)` — no test
- `ItemRepository.sync()` — no test
- Error paths for `save()` and `delete()` — not tested

**Action**: Generate characterization tests for uncovered methods before refactoring.
Use `characterization-test-generator` skill.
```

#### Red Light — Stop

```markdown
## Refactor Guard: 🔴 STOP

**Files**: ItemManager.swift, ItemRepository.swift
**Test coverage**: None (0 tests found)

**Action**: Do NOT refactor until characterization tests exist.
Refactoring without tests is like surgery without monitoring —
you won't know if you killed the patient.

Run `characterization-test-generator` on:
1. ItemManager (7 public methods)
2. ItemRepository (5 public methods)

Then re-run this guard.
```

### Step 6: Post-Refactor Verification

After refactoring is complete:

```bash
# Run all tests
xcodebuild test -scheme YourApp

# Check specifically for regressions
xcodebuild test -scheme YourApp \
  -only-testing "YourAppTests/ItemManagerCharacterizationTests"
```

- [ ] All pre-existing tests still pass
- [ ] Characterization tests still pass
- [ ] No new warnings or errors
- [ ] Public API surface unchanged (unless intentionally changed)

## Refactoring Safeguards

### Safe Refactorings (Low Risk)

These typically don't change behavior:
- Rename variable/method/type
- Extract method/function
- Inline temporary variable
- Move method to another type (keeping same behavior)
- Extract protocol from class

### Risky Refactorings (Need Good Coverage)

These can change behavior:
- Change method signature
- Reorder operations
- Replace algorithm
- Merge/split classes
- Change data structure
- Introduce async/await to sync code
- Migration between frameworks (CoreData → SwiftData)

### Migration Refactorings (Need Comprehensive Coverage)

Full contract tests recommended:
- Architecture change (MVC → MVVM)
- Framework migration (UIKit → SwiftUI)
- Storage migration (UserDefaults → SwiftData)
- Concurrency model change (GCD → async/await)

## Output Format

```markdown
## Refactor Guard Report

**Scope**: [Files/classes to be refactored]
**Verdict**: ✅ PROCEED / ⚠️ NEEDS TESTS / 🔴 STOP

### Coverage Summary
| Class | Public Methods | Tests | Coverage | Verdict |
|-------|---------------|-------|----------|---------|
| ItemManager | 9 | 12 | 89% | ✅ |
| ItemRepository | 5 | 2 | 40% | ⚠️ |

### Missing Coverage
- [ ] `ItemRepository.sync()` — needs characterization test
- [ ] Error path for `ItemRepository.delete()` — untested

### Recommendations
1. [Action item]
2. [Action item]
```

## References

- `testing/characterization-test-generator/` — generates tests this guard may require
- `testing/tdd-feature/` — for building new code during refactor
- Martin Fowler, *Refactoring: Improving the Design of Existing Code*

Overview

This skill is a pre-refactor safety checklist that verifies test coverage before any AI-assisted refactor of Swift code. It prevents regressions by assessing existing tests and generating characterization tests when coverage is insufficient. Use it as a gate: it either green-lights a refactor or requires tests first.

How this skill works

It inspects the files and public API surface you intend to change, scans test targets for relevant XCTest or test annotations, and evaluates coverage and test quality. If coverage is low or tests are missing or low-quality, it produces characterization tests that capture current behavior and reports a clear verdict: PROCEED, NEEDS TESTS, or STOP. After refactoring it recommends targeted test runs to confirm no regressions.

When to use it

  • Before asking an AI to "refactor this", "clean this up", or "modernize" code
  • When extracting, inlining, renaming, or simplifying existing code
  • Prior to migrating patterns or frameworks (e.g., MVC→MVVM, CoreData→SwiftData)
  • When making risky changes like changing algorithms, signatures, or concurrency model
  • Any time you want to ensure behavior stays the same during an automated refactor

Best practices

  • Identify exact files/classes and public API surface before running the guard
  • Require tests that assert observable outputs/state, not implementation details
  • Cover happy paths, error paths, edge cases, and async behavior
  • Generate characterization tests for uncovered public methods before changing behavior
  • Run specific test targets after each change (xcodebuild test with only-testing filters)

Example use cases

  • User asks an AI bot to refactor ItemManager.swift — guard verifies tests and generates missing characterization tests
  • Planning a migration from CoreData to SwiftData — guard requires comprehensive contract tests first
  • Request to modernize a ViewModel to async/await — guard checks for async tests and edge case coverage
  • Small refactor like renaming methods — guard confirms low-risk and green-lights with existing coverage

FAQ

What happens if no tests exist?

The guard returns a STOP verdict and generates characterization tests for public methods; do not refactor until those tests pass.

How does it judge test quality?

It flags tests that verify implementation details, always-true assertions, or missing async awaits, and prefers tests asserting observable state and error cases.

Can I skip the guard?

Skipping risks silent regressions; use the guard to ensure safety, especially for risky or cross-cutting changes.