Refactoring means changing code's internal structure without changing what it does — but 'without changing what it does' is a promise that's only trustworthy if you can actually verify it. Small steps and a passing test suite are what make that promise real instead of hopeful.
3 min read
Refactoring specifically means restructuring existing code — renaming, extracting a function, reorganizing how logic is split up — without changing its observable behavior at all. This definition matters because it's precise: if behavior changes even slightly, it wasn't a refactor, it was a feature change or a bug fix wearing a refactor's name — and conflating the two is exactly what makes refactoring feel riskier than it needs to be.
A large, sweeping refactor — restructuring a big chunk of a system all at once, then testing at the very end — has a specific, common failure mode: if something breaks, there are now dozens of changes to search through to find which one caused it, and reverting isn't simple either, since the "refactor" is one giant, tangled unit. This is exactly why real refactoring discipline favors many small, independently verifiable steps over one large one.
Refactoring's entire safety guarantee — "behavior didn't change" — is only verifiable if there's a reliable way to check behavior automatically. Without tests, "refactoring" a piece of code is really just hoping nothing broke, discovered only whenever someone happens to notice — often much later, and far from the actual change that caused it. This is the direct, practical reason "characterization tests" (tests written specifically to capture what code currently does, even if that behavior seems questionable) are often the first real step before refactoring genuinely untested code — you can't safely change what you can't verify.
Real refactoring proceeds in small, individually-verifiable moves — extract one function, rename one variable, inline one unnecessary wrapper — running tests after each single step, and committing each one that passes before moving to the next. This might feel slower step by step than making several changes at once, but it's dramatically faster in total time whenever something does break, since the broken step is always the very last thing you touched.
# Before: one long function doing three separate things
def process_order(order):
total = sum(item.price * item.qty for item in order.items)
total = total * (1 - order.discount_pct)
send_email(order.customer_email, f"Your total: ${total}")
return total
# After one small, verified step: extract the pricing logic only —
# nothing else about behavior changes, tests still pass identically
def calculate_total(order):
total = sum(item.price * item.qty for item in order.items)
return total * (1 - order.discount_pct)
def process_order(order):
total = calculate_total(order)
send_email(order.customer_email, f"Your total: ${total}")
return totalA single commit that both refactors code AND fixes a bug or adds a feature is
genuinely hard to review and hard to revert safely — a reviewer (or future
git bisect) can't tell which part of the diff is "just restructuring, trust
it" versus "an actual behavior change, scrutinize this." Keeping refactoring
commits pure — structural change only, verified by the exact same passing
tests before and after — makes both code review and future debugging
meaningfully easier.
Not every messy piece of code needs refactoring immediately — the practical trigger worth following: refactor the specific code you're already about to touch for an unrelated reason (colloquially, the "boy scout rule" — leave code a little better than you found it), rather than scheduling a separate, dedicated refactoring project for code nobody currently needs to change. Code that works and that nobody is touching rarely needs to be your priority, no matter how it looks.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What does 'refactoring' specifically mean, as defined in this lesson?
2. Why are many small, independently-tested refactoring steps generally safer than one large sweeping refactor?
3. Why does refactoring require tests to be genuinely safe?
Software Engineering Fundamentals