Refactor Without Behavior Change
Pin current behavior with characterization tests, then restructure in small always-green steps so the diff provably changes no behavior.
Use when: Use when restructuring, renaming, extracting, or moving existing code where the requirement is that observable behavior stays identical.
The skill file
Claude Code · SKILL.mdSKILL.md3545 chars
# Refactor Without Behavior Change
A refactor that changes behavior is a bug with good intentions. The contract: tests green before, green after, and at every step in between.
## Step 0 — Pin the behavior
1. Run the existing tests for the target code and note coverage of the lines you'll touch. Uncovered branches are where refactors silently break things.
2. Where coverage is thin, write **characterization tests**: call the code with representative inputs and assert whatever it *currently* returns — including outputs that look wrong. You are documenting reality, not the spec. Mark suspected bugs with a comment (`// current behavior; possibly a bug — see #123`) and file an issue; do not fix them in this change.
3. Cheap characterization tricks: snapshot/golden-file tests on serialized output; running the function over a corpus and hashing results; capturing real production inputs as fixtures.
4. If the code has side effects (DB writes, emitted events, files), assert on those too — return values alone under-specify behavior.
## Step 1 — Plan small steps
Decompose the restructuring into moves that each take minutes, not hours:
- Rename → Extract function → Move function → Inline → Change signature (with a temporary delegating shim) → Replace conditional with polymorphism.
- Prefer the IDE/LSP rename and extract operations over hand-editing; they don't typo.
- For a big migration (old API → new API), use parallel-change: introduce the new path, migrate callers one by one with tests green after each, delete the old path last.
## Step 2 — Execute, staying green
- Run the test suite after **every** step. If a step breaks tests and the fix isn't obvious within a couple of minutes, `git checkout -- .` and take a smaller step. Reverting is cheaper than debugging a tangled half-refactor.
- Commit after each green step or small group of steps. The commit message says which refactoring move it was: `refactor(billing): extract proration calculation from Invoice.total`.
- No behavior changes ride along. No "while I'm here" fixes, no upgraded dependencies, no improved error messages. If you find a real bug, note it, finish the refactor, fix it in a separate commit with its own failing test.
## Tooling that keeps you honest
- A coverage report before starting tells you where characterization tests are needed; the same report after confirms you didn't orphan code.
- Type checker and linter run with the suite at every step — a refactor that compiles on the happy path can still break an `import` three modules away.
- For pure functions, consider a quick property check: run old and new implementations side by side over generated inputs and assert equal outputs, then delete the old one.
## Hard rules
- Never refactor and change behavior in the same commit. Reviewers cannot verify "this diff is safe" otherwise.
- Never weaken an assertion to make a refactor pass. A failing characterization test means the refactor changed behavior — that's the system working.
- Public API renames need a deprecation shim unless you migrate every caller in the same change and the API is internal.
- Performance counts as behavior at the boundaries: if the code is on a hot path, take a before/after measurement.
## Done means
- [ ] Same test suite passes before and after (plus the new characterization tests).
- [ ] `git log` shows a sequence of small, individually-green commits.
- [ ] Diff contains zero changes to expected values in tests.
- [ ] Any discovered bugs are filed, not silently fixed or silently kept.
Install
drop into your repoSave this to your project or home directory so Claude Code can load it.
path~/.claude/skills/refactor-without-behavior-change/SKILL.md
Discussion
What people are saying
Forks
0 forks of this skillLoading forks…