Install any skill in seconds. Free to start, no credit card required.
Get Started Free →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.
.claude/skills/rshankras-characterization-test-generator/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 15% | 0% |
| case-02 | ✗→✓ | ▲ Improved | 22% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 62% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 62% | 0% |
| case-20 | ✗→✓ | ▲ Improved | 9% | 0% |
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:
swiftimport Testing @testable import YourApp @Suite("Characterization: ItemManager") struct ItemManagerCharacterizationTests { // Tests document CURRENT behavior, not ideal behavior }
swift@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) }
swift@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) }
swift@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) }
swift@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") }
swift@Test("current behavior: throws on empty title") func throwsOnEmptyTitle() { let validator = ItemValidator() #expect(throws: ValidationError.emptyField("title")) { try validator.validate(Item(title: "", date: .now)) } }
swift@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:
swift// ❌ 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:
swift// CHARACTERIZATION: This returns 11 not 10 due to inclusive range. // Don't "fix" this until intentionally changing behavior. #expect(range.count == 11)
swift@Suite("Characterization: ItemManager") @Tag(.characterization) struct ItemManagerCharacterizationTests { ... } // Define the tag extension Tag { @Tag static var characterization: Self }
bash# Run only characterization tests xcodebuild test -scheme YourApp \ -only-testing "YourAppTests/ItemManagerCharacterizationTests"
markdown## 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 testsOther measured skills in the registry, with their headline benchmark lift.