Dhaka, Bangladesh
Case Studies

Refactoring Legacy Code With No Test Suite

Cover image for the case study "Refactoring Legacy Code With No Test Suite"
The risk of refactoring 13+ dependent files with zero test coverage, managed through compensating practices, with no regressions shipped - but tests next time.
I refactored PostX's shared CSS component system - a system with 13+ dependent files - in a codebase with no automated test suite. The risk was real: any regression I introduced wouldn't be caught until someone noticed it visually or in production. I managed it through a component-by-component approach, keeping each commit focused and the working branch in sync with main throughout. It worked. But I'd add test coverage before starting if I did it again.
Zero test files in the codebase, 13+ dependent files at risk, zero regressions shipped through manual verification alone.
TL;DR summary
The riskNo *.test.js or *.spec.js files existed in PostX at the time of the refactor
Scope of change13+ files depended on the shared component system being refactored
How I managed itComponent-by-component commits, continuous branch sync, manual UI verification
Did it work?Yes - no regressions shipped
Paired case studyCSS architecture refactoring →
The shared CSS component system and the rest of the JS/React codebase both carried no automated test coverage, a deferral common in older plugin codebases that had compounded by the time of the refactor.
PostX is a WordPress plugin used by 40,000+ users. Its shared CSS component system - the file that controlled the appearance of every UI field across every block's settings panel - had no automated tests. Neither did the rest of the JS/React codebase. This is common in older plugin codebases. PostX's frontend code dates back to 2019, and test infrastructure is often the thing that gets deferred when a small team is moving fast on features. By the time I was refactoring the shared stylesheet, that deferral had compounded: I was making structural changes to a system 13+ other files depended on, with no safety net to catch what I might break.
Removing a style rule one field type depended on could silently break a different field type in a panel never opened, with no automated check to catch it.
To be specific: the shared component system controlled the visual behavior of toggles, color pickers, dropdowns, typography controls, spacing sliders, and more - across every block settings panel in the plugin. A regression here wouldn't be subtle. If I accidentally removed a style rule that a different field type depended on, a toggle might render incorrectly in a completely different block's panel - one I hadn't opened, wasn't thinking about, and had no automated check against.
Four compensating practices: one component per commit, continuous branch sync with main, manual UI verification per component, and focused, bisectable commit messages.
One component per commit. I went field type by field type - toggle, range, select, color, typography, dimension, accordion, and so on. Each commit extracted one component's styles into its own file and nothing else. Continuous branch sync. Rather than letting the working branch drift from main and doing a large merge at the end, I kept it in sync throughout. Merge conflicts on a shared stylesheet are painful - catching them early is much safer than resolving a month's worth at once. Manual UI verification per component. After each extraction, I opened the relevant settings panels in the WordPress editor and checked that every field type still rendered correctly - not just the one I'd moved, but adjacent ones that shared the same file. Focused commit messages. Every commit in the series was named refactor: redesign [component name]. That naming made it easy to bisect if something went wrong - I could identify exactly which extraction introduced a problem and revert just that commit.
Automated tests would have caught regressions after every extraction; instead, checking the main dependent files meant nothing broke that was caught - not the same claim as nothing breaking.
Looking back clearly: the risk wasn't the refactoring approach. The risk was the missing test coverage that should have existed before I started. Tests written against the existing behavior - "toggle renders with correct spacing," "color picker displays at expected width" - would have caught regressions automatically after each extraction. Instead, I was the test suite. That scales poorly, relies on catching things visually, and doesn't protect against subtle regressions in states you didn't manually check. I checked the main dependent files. I can't say I checked every rendering permutation across every block. I can say nothing broke that we caught - but absence of evidence isn't the same as evidence of absence.
Refactoring without tests is like moving furniture in the dark - careful, but relying on not bumping into things rather than being able to see where they are.
Refactoring without tests is like moving furniture in the dark. You can do it. You can be careful. You can go slowly and feel your way around. But you're relying on not bumping into things rather than being able to see where things are. Most production codebases have areas with no test coverage. The skill isn't pretending the risk doesn't exist - it's managing it explicitly, naming it to the team, and building in compensating practices while also being honest that those practices are a substitute for the right solution, not equivalent to it.
The three questions this case study answers: how to safely refactor without tests, whether to add tests before or after, and what the actual risk is.
A single large change with no test suite has no safe rollback point if something breaks - going one component at a time meant each commit was independently verifiable and the branch never drifted far enough from main to make a bad change hard to isolate. Tests written against the existing behavior give you a comparison point to catch regressions as you move code. Written after, they only describe the refactored version - by then you've already lost the baseline they'd need to be checked against. Part of the PostX frontend refactoring case study series →
On this page
Quick answer
TL;DR
The situation
What the risk actually looked like
How I managed it
What made this riskier than necessary
The broader lesson
FAQ
Why go component by component instead of refactoring everything at once?
Why does it matter that tests would have been written before the refactor rather than after?
Why is refactoring shared components without tests riskier than a typical change?
Because a shared component's blast radius isn't visible from the file you're editing - with 13+ files depending on one system, a missed side effect can surface in a panel you never opened during manual verification.