How Do You Refactor Code That Has No Tests?
Have Claude write characterization tests that record what the code does today, quirks included. Check that they pass on the unchanged code and fail on deliberately broken code. Then lock them and refactor. A green suite then tells you nothing observable changed.
[path] has no tests, and I want to refactor it. Don't change it yet. Read it and write characterization tests in [test path] that record what it does today, including odd outputs and edge cases. Use real inputs from [fixtures, logs or examples]. Test through the public interface, not private helpers, since those will move. If behavior looks like a bug, keep the test matching today's behavior, add the comment `CURRENT BEHAVIOR, POSSIBLE BUG`, and list it for me. Run them with [test command] and show me the output. Then prove they can fail: break the code in three different ways, one at a time, run the tests each time, and report which tests caught it. Restore the code exactly, and show `git status` at the end.
- 1Record today's behavior
- 2Prove the tests can fail
- 3Lock the tests
- 4Refactor
Try it now
- 1Commit or stash your work first, so a clean tree makes any leftover change obvious.
- 2Send the prompt in Claude Code, and read the list of possible bugs. Those are things you were about to preserve.
- 3Check the three deliberate breaks. If one wasn't caught, ask for a test that catches it.
- 4It worked if
git statusshows only new test files. Commit them separately before refactoring.
Use it when
Legacy or inherited code with no tests, particularly code you don't fully understand. The less you trust it, the more this pays off. It suits Claude Code, since Claude needs to run the tests and break the code on purpose.
Skip it when
Code that already has good behavioral tests, which you can refactor against directly. Code you're about to delete or rewrite from scratch, because you'd be pinning behavior you don't want. And code with outputs that change on every run, unless you first control the clock and random inputs.
Why it works
Anthropic's write-up on large code migrations makes this the prerequisite: first build a test harness, and confirm it passes on the original and fails on code broken on purpose. If no suite exists, it says to have Claude build one, using the original code as ground truth. Breaking the code on purpose is the part that gives you confidence, because tests Claude wrote by reading the code might only restate it. Locking matters too. Anthropic's prompting docs warn that Claude can focus on making tests pass rather than on general solutions, and suggest telling it plainly that removing or editing tests isn't acceptable. A hook makes that a rule rather than a request.
Example
calculatePrice() has no tests and handles tiered discounts. Claude writes tests using real order values from fixtures/orders.json, and one records that a $19.995 line rounds down. That gets flagged as a possible bug. You refactor the function into three steps with the rounding test still passing, then fix the rounding later in its own commit, where the changed test shows exactly what changed.
Going further
Lock the tests with a PreToolUse hook. Anthropic's hooks guide has a protect-files.sh example that checks the target path against a list of patterns and exits with code 2 to block the edit, passing your message to Claude. Add tests/characterization/ to that list and register it with the matcher Edit|Write in .claude/settings.json. It needs jq. Know its limit: the matcher covers those tools only, so a sed run through Bash isn't caught. Remove the lock when you start the deliberate bug fixes, because those tests are supposed to change then.
Common mistakes
- Fixing bugs while writing the tests. The tests then describe behavior the old code never had, so you can't tell a refactor break from your fix.
- Testing private helpers you're about to move. The tests break on the refactor even when behavior holds, which teaches you to ignore them.
- Skipping the deliberate breaks. Tests that always pass feel safe and catch nothing.
- Committing tests and refactor together. A red test can't tell you which one caused it.
Sources
- Code migrations with Claude Code, Claude blog
- Prompting best practices, Claude Platform Docs
- Automate actions with hooks, Claude Code Docs
Checked 6 November 2026. Products change often, so confirm details with the vendor.