skills/testing/characterization-test-generator/SKILL.md
Generates tests that capture current behavior of existing code before refactoring. Use when you need a safety net before AI-assisted refactoring or modifying legacy code.
npx skillsauth add rshankras/claude-code-apple-skills characterization-test-generatorInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Generate tests that document what existing code actually does — not what it should do. These tests capture current behavior so you can refactor with confidence, especially when using AI to modify code.
Use this skill when the user:
AI refactoring is powerful but risky:
Glob: **/*.swift (in the target module)
Grep: "class |struct |enum |protocol |func " (public API surface)
Identify:
For each public method, classify:
| Category | Example | Test Strategy |
|----------|---------|---------------|
| Pure computation | calculate(a, b) -> c | Input/output pairs |
| State mutation | addItem(_:) modifies array | Before/after state checks |
| Async operation | fetchData() async -> [Item] | Mock dependencies, verify results |
| Side effect | save() writes to disk | Verify mock was called |
| Event emission | delegate?.didUpdate() | Capture delegate calls |
| Error path | Throws on invalid input | Verify correct error type |
Characterization tests should be clearly labeled:
import Testing
@testable import YourApp
@Suite("Characterization: ItemManager")
struct ItemManagerCharacterizationTests {
// Tests document CURRENT behavior, not ideal behavior
}
@Test("current behavior: calculates total with tax")
func calculatesTotal() {
let calculator = PriceCalculator()
let result = calculator.total(subtotal: 100.0, taxRate: 0.08)
// Document actual behavior — even if the rounding seems wrong
#expect(result == 108.0)
}
@Test("current behavior: addItem appends and sorts by date")
func addItemSortsbyDate() {
let manager = ItemManager()
let older = Item(title: "A", date: .distantPast)
let newer = Item(title: "B", date: .now)
manager.addItem(older)
manager.addItem(newer)
// Document actual ordering behavior
#expect(manager.items.first?.title == "A")
#expect(manager.items.last?.title == "B")
#expect(manager.items.count == 2)
}
@Test("current behavior: loadItems returns cached data when offline")
func loadItemsOffline() async throws {
let mockNetwork = MockNetworkClient(shouldFail: true)
let mockCache = MockCache(items: [Item.sample])
let service = ItemService(network: mockNetwork, cache: mockCache)
let items = try await service.loadItems()
// Document: falls back to cache when network fails
#expect(items.count == 1)
#expect(items.first?.title == Item.sample.title)
}
@Test("current behavior: save writes to UserDefaults")
func saveWritesToDefaults() {
let defaults = MockUserDefaults()
let settings = SettingsManager(defaults: defaults)
settings.setTheme(.dark)
settings.save()
// Document: saves as string, not as enum raw value
#expect(defaults.lastSetValue as? String == "dark")
#expect(defaults.lastSetKey == "app_theme")
}
@Test("current behavior: throws on empty title")
func throwsOnEmptyTitle() {
let validator = ItemValidator()
#expect(throws: ValidationError.emptyField("title")) {
try validator.validate(Item(title: "", date: .now))
}
}
@Test("current behavior: handles nil optional gracefully")
func handlesNilOptional() {
let parser = DataParser()
let result = parser.parse(data: nil)
// Document: returns empty array on nil, doesn't crash
#expect(result.isEmpty)
}
@Test("current behavior: handles empty collection")
func handlesEmptyCollection() {
let aggregator = StatsAggregator()
let stats = aggregator.compute(values: [])
// Document: returns zeroes, not NaN or crash
#expect(stats.average == 0.0)
#expect(stats.count == 0)
}
This is the key step. For each test:
// ❌ Wrong — this is what you WANT it to do
#expect(result == expectedCorrectValue)
// ✅ Right — this is what it ACTUALLY does
#expect(result == actualCurrentValue) // Note: off-by-one, but current behavior
Add comments for surprising behavior:
// CHARACTERIZATION: This returns 11 not 10 due to inclusive range.
// Don't "fix" this until intentionally changing behavior.
#expect(range.count == 11)
@Suite("Characterization: ItemManager")
@Tag(.characterization)
struct ItemManagerCharacterizationTests { ... }
// Define the tag
extension Tag {
@Tag static var characterization: Self
}
# Run only characterization tests
xcodebuild test -scheme YourApp \
-only-testing "YourAppTests/ItemManagerCharacterizationTests"
## Characterization Tests Generated
**Module**: [Module name]
**Classes tested**: [List]
**Tests generated**: [Count]
### Coverage Summary
| Class | Methods Covered | Edge Cases | Notes |
|-------|----------------|------------|-------|
| ItemManager | 5/7 | 3 | 2 private methods skipped |
| PriceCalculator | 3/3 | 2 | All public API covered |
### Files Created
- `Tests/CharacterizationTests/ItemManagerCharacterizationTests.swift`
- `Tests/CharacterizationTests/PriceCalculatorCharacterizationTests.swift`
### Surprising Behaviors Found
- `PriceCalculator.total()` rounds DOWN, not to nearest cent
- `ItemManager.sort()` is unstable — equal dates may reorder
### Ready to Refactor
All [X] characterization tests passing. Safe to refactor with AI.
Run `xcodebuild test` after each change to verify no behavior changed.
tdd-bug-fix instead)generators/test-generator/ — for standard test generation (not characterization)testing/tdd-refactor-guard/ — pre-refactor checklist that uses these testsdevelopment
Build, install, and launch an iOS app on a physical iPhone or iPad entirely from the command line (no Xcode GUI), using xcodebuild + devicectl. Use when the user wants to run, test, or screenshot their app on a real device without opening Xcode.
development
Comprehensive iOS development guidance including Swift best practices, SwiftUI patterns, UI/UX review against HIG, and app planning. Use for iOS code review, best practices, accessibility audits, or planning new iOS apps.
development
Build, install, launch, and screenshot an iOS app in the Simulator to verify a change visually. Use when the user wants to run the app, see a change live, screenshot the running app, or confirm a UI fix actually works (not just that it compiles).
development
Audits skills in this repo for consistency, API drift, and structural gaps. Produces a prioritized report grouped by severity (Critical/High/Medium/Low). Use when asked to "audit skills", "check the skill repo for drift", or when planning bulk skill cleanup. Read-only — does not apply fixes.