characterization-test-writer
Skill2 starsChanged 38 days agoMITView on GitHub
Writes pinning and characterization tests that lock in the current behavior of untested legacy code - bugs included - before a refactor, so any later behavior change trips an alarm. Use when someone says "I need a safety net before refactoring this", "this module has no tests and I have to change it", "capture what this function does today", or the correct behavior is simply whatever the code does now. Do NOT use for writing tests for new behavior built red-green - use tdd-expert instead; do NOT use to decide which code is worth testing or to rank coverage gaps - use coverage-gap-finder instead; do NOT use to verify existing assertions are strong - use mutation-test-runner instead.
In this repository8 other files it gives its agents.
- business-rule-extractorSkill
- dead-code-eliminatorSkill
- dependency-risk-auditSkill
- framework-upgraderSkill
- language-version-migratorSkill
- monolith-decomposerSkill
- strangler-fig-plannerSkill
- tdd-expertSkill
The file
We split this skill into the rules it gives an agent
11 rules. About 0 are common, 10 are distinctive, and 1 is very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈10
Distinctive · estimate
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 27
Write a deliberately failing assertion, run it, read the real value from the failure message, and paste it in:
- Distinctive: line 48
When output genuinely cannot be made value-deterministic (large reports, generated documents), use golden-master / approval testing …
- Distinctive: line 65
Do NOT assert what the code *should* do - that is a normal unit test.
- Distinctive: line 66
Do NOT fix bugs discovered while pinning.
- Distinctive: line 67
Do NOT perform a large refactor to make code testable before the net exists.
- Distinctive: line 68
Do NOT characterize code about to be deleted, or code already covered by intention-revealing unit tests.
- Distinctive: line 69
Do NOT hand-write expected values from reading the code - every asserted value must come from a real run.
- Distinctive: line 70
Do NOT keep these tests forever.
- Distinctive: line 74
Every asserted value was observed from a real run, never reasoned to.
- Distinctive: line 75
Every branch in the refactor's blast radius is covered before a line of the refactor is written.
- Very specific: line 78
Every pinned-but-suspect output carries a pins_known_bug_* tag so a bug is never silently promoted to a requirement.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Characterization Test Writer
Pin what legacy code currently does - bugs included - so a refactor that changes behavior fails loudly instead of silently. The costly mistake this prevents is the "clean" refactor that quietly changes an output nobody noticed was load-bearing, discovered weeks later in production. A characterization test records reality; it never judges it.
Operating procedure
Order matters: the seam must exist before you can capture output, output must be captured before you can stabilize it, and coverage is only measurable once tests run.
Step 1: Gather inputs
- The exact function/module about to change, and the refactor's blast radius - which code paths will the change touch? Only those need pinning.
- The dependency list: database, clock, RNG, network, filesystem, globals, environment. Each is a nondeterminism source to control in Step 4.
- Any recorded production inputs or logs available for replay. Label assumptions about "typical inputs" as guesses.
Step 2: Find the smallest seam
Untested legacy code reaches for databases, clocks, network, and globals, so it usually cannot be called in isolation. Introduce the minimum seam that lets you invoke it: extract a method, parameterize a constructor, inject the dependency, or subclass-and-override (the "subclass to sense" technique from Working Effectively with Legacy Code). Do not refactor the body before the net exists - seam-introduction edits must be mechanical and behavior-preserving (rename, extract, inject), nothing that changes logic.
Step 3: Capture actual output, not expected
Write a deliberately failing assertion, run it, read the real value from the failure message, and paste it in:
def test_characterize_shipping_for_oversize_order():
result = calc_shipping(weight_kg=32, region="EU")
assert result == "PLACEHOLDER" # run -> failure says: AssertionError: 47.6 != PLACEHOLDERbecomes, after one run:
def test_characterizes_oversize_eu_shipping():
result = calc_shipping(weight_kg=32, region="EU")
assert result == 47.6 # observed 47.6, not the 45.0 the spec claims - see pins_known_bug belowIf the captured value looks wrong, pin it anyway and tag it (pins_known_bug_ prefix or a comment) so the team decides its fate later. Fixing it now defeats the purpose: the refactor must be provably behavior-neutral first, and the bug fix becomes its own reviewable change afterward.
Step 4: Control nondeterminism until the suite passes twice in a row
- Freeze time (freezegun, libfaketime, an injected Clock), seed RNGs, stub network and I/O, and snapshot database state.
- When output genuinely cannot be made value-deterministic (large reports, generated documents), use golden-master / approval testing (ApprovalTests, snapshot files): record the full output blob once, diff on every run, and scrub volatile fields (timestamps, IDs) in the comparison.
- The acceptance test for this step is literal: run the suite twice consecutively with zero flakes.
Step 5: Cover every branch the refactor will touch
Run branch coverage and add cases until every branch in the refactor's blast radius is exercised - branch coverage, not line coverage, because an if/else can be 100% line-covered with one arm never taken. Use parameterized tests and replay of recorded production inputs to reach edge cases you would never invent (empty input, nulls, boundary values, error paths). Code outside the blast radius does not need pinning - this is a net for one refactor, not a full suite.
Step 6: Make failures legible
Name each test for the behavior it pins (characterizes_returns_zero_for_empty_cart, not test_method_3) and keep each assertion narrow, so one regression breaks one narrowly-scoped test and the failure message names the changed behavior.
Deliverable
Produce a characterization suite plus a one-paragraph coverage note: which paths in the blast radius are pinned, the twice-in-a-row green run confirmed, and a tagged list of pinned-but-suspect outputs (pins_known_bug_*) handed to the team for post-refactor triage.
Do NOT
- Do NOT assert what the code should do - that is a normal unit test; characterization asserts what it does.
- Do NOT fix bugs discovered while pinning; tag them, pin them, fix them after the net exists.
- Do NOT perform a large refactor to make code testable before the net exists; introduce only the minimum mechanical seam.
- Do NOT characterize code about to be deleted, or code already covered by intention-revealing unit tests.
- Do NOT hand-write expected values from reading the code - every asserted value must come from a real run; reasoning to a value smuggles in your model of the code, which is exactly what is untrusted.
- Do NOT keep these tests forever; they are scaffolding. Replace them with intention-revealing tests once the refactor lands and the behavior is understood.
Quality bar
- Every asserted value was observed from a real run, never reasoned to.
- Every branch in the refactor's blast radius is covered before a line of the refactor is written.
- The suite passes twice in a row with no flakiness - nondeterminism is fully pinned.
- One regression breaks one narrowly-named test whose name states the broken behavior.
- Every pinned-but-suspect output carries a
pins_known_bug_*tag so a bug is never silently promoted to a requirement.
Escalation
Deciding which untested code deserves this treatment is coverage-gap-finder's job. Proving the finished suite's assertions actually bite is mutation-test-runner's. If the refactor is a service extraction rather than an in-process change, sequence it with monolith-decomposer, and capture the business rules inside the code with business-rule-extractor.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

