How to review a refactor when the claim is no behavior change
Othman Shareef · August 17, 2026 · 6 min read · The Craft of Code Review
“Pure refactor, no behavior change” is the most trusted sentence in code review and the least verified. Reviewers hear it, downshift into skim mode, and approve two thousand moved lines on vibes. Knowing how to review a refactor starts with reclassifying that sentence: it is not context, it is a claim, and claims need evidence. The author believes it, sincerely, every time. The production incident does not care about sincerity.
A refactor is a claim that needs evidence
The reviewer’s job in a refactor is not to admire the new structure; it is to verify the equivalence. That reframing changes what you ask for. Instead of “does the new code look right”, the questions become: what would show me the old and new code behave identically, which parts of this diff could a machine verify, and which parts require human judgment? A refactor PR that arrives as one monolithic commit with edited tests and a cheerful description offers no evidence at all, and the correct review response is not heroic reading. It is asking for the change to be restructured so that its safety is checkable.
How to review a refactor commit by commit
Commit structure is the single biggest lever, which is why atomic commits matter more in refactors than anywhere else. The reviewable shape is a sequence where each commit is one kind of change: a move-only commit, a rename-only commit, an extract-function commit, then the one commit that genuinely rewrites logic. Reviewed in order, the mechanical commits take seconds each and the judgment commit gets your full attention; reviewed flattened, the judgment change hides inside ten thousand lines of noise. This is where a review surface that scopes the diff per commit stops being a convenience and becomes the method (disclosure: commit-scoped diffs are a core feature of Pyor, ours, precisely because refactor review collapses without them). If the commits are not structured this way, the most valuable review comment is to ask for it: it costs the author an hour and saves the behavior.
Separate mechanical from judgment
Every refactor decomposes into two kinds of change, and they deserve opposite treatment:
- Mechanical: renames, file moves, reordering, extract-with-identical-body. These are machine-checkable. Your job is not to read them but to confirm the tooling agrees they are what they claim to be.
- Judgment: restructured conditionals, changed data flow, merged duplicate paths, new abstractions. These are where equivalence can silently fail, and they get the closest reading in the PR.
The ratio matters too. A refactor that is 95% mechanical and 5% judgment is a fast, safe review when the two are separated, and an unreviewable blob when they are mixed. Mixing them is how a flipped >= travels inside a moved block, invisible because the whole block is “just moved”.
The test suite is the harness, so it should not change
In a pure refactor, the strongest evidence available is boring: every existing test passes and the test files are untouched. The old assertions, written against the old code, now hold against the new code; that is as close to a behavioral proof as review gets. Which is why edited tests inside a refactor deserve immediate attention. Sometimes the edit is legitimate: the tests asserted implementation details (a private method name, an internal call order) that the refactor relocated. Fine, but then the description must say so, per test, because the alternative reading is that the behavior changed and the tests were updated to agree with it. Tests rewritten wholesale alongside the code they verify is the same failure we documented in the test-rewrite failure mode: nothing independent is left standing.
Renames and moves without re-reading everything
Nobody should re-read a thousand lines to confirm a file moved. Git already detects renames, and git diff --color-moved distinguishes blocks that moved intact from blocks that changed in flight, which is exactly the question a reviewer has. For refactors that land through force-pushed cleanups, git range-diff compares the old and new versions of the branch so re-review costs minutes instead of a second full pass. The principle underneath all the tooling: never spend human attention verifying something a machine can verify. Spend it where the machine is blind, on whether the new structure means the same thing as the old one.
The sneaked-in behavior change
The classic refactor failure is not a botched extraction; it is the “while I was here” fix. A null check added because it seemed obviously missing, a condition tightened, a default corrected: each one is a behavior change traveling under a no-behavior-change flag, unreviewed because the reviewer was told there was nothing to review. Some of those fixes are even right, which makes it worse: the wrong ones inherit the trust the right ones earned. The triage discipline from reviewing large PRs applies directly: sort the diff into what claims to be mechanical and what does not, verify the claim with tooling, and treat every line that fails verification as new, unreviewed logic. When you find a genuine fix hiding in a refactor, the answer is not to reject the fix. It is to ask for it as its own commit, with its own test, wearing its own name.