Craft of code
Characterization tests
Pin what legacy code does today before you clean it, so a refactor cannot drift in silence.
Legacy code is not old code. Michael Feathers' useful definition: code without tests, or without tests you trust. Untested code resists change because every edit is a bet. A characterization test documents what the code does today, including the weird parts, so you can move structure later without changing output by accident.
You are not asserting the ideal. You are locking reality. Once those tests are green, you may tidy toward maintainable code. Change behavior in a separate commit.
Concept
Refactoring is a behavior-preserving transformation. If users can tell the output changed, you did not refactor — you changed behavior. That is fine, but do it separately.
Walking through it
Before you touch a scary invoice module, pin a production case you are not ready to "fix."
// Sketch: pin today's weird but load-bearing rounding.
test("invoice total matches production fixture 2024-11-02", () => {
const input = readFixture("invoices/legacy-case-77.json")
expect(legacyInvoiceTotal(input)).toBe(1044) // cents; do not "fix" yet
})Generate fixtures from production samples and scrub PII. Cover the paths you will touch. Golden-master or snapshot tests work when the output is large and stable enough to review when it changes.
A large pricing engine with no tests is the same move at larger scale: pin production quotes first. Only then extract, flag, and ramp. See incremental migrations for the later weeks.
Common mistakes
- Skipping tests because you are careful. You are not. Characterization is the care.
- Asserting the ideal while characterizing. Freeze today's rounding first. Fix it later.
- Abstracting too early. Do not invent a new design while you are still learning what the old one does.
- Hiding a behavior change inside the first cleanup PR. Reviewers miss it; bisect lies.
Check yourself
-
A characterization test fails after a rename. Did you refactor, or change behavior? If the output changed, you changed behavior. A rename alone should stay green.
-
You notice an off-by-one while writing fixtures. Do you fix it in the same PR? No. Finish the structural work green against the pin, then open a second PR with a failing test for the bug.
Key takeaways
- Legacy means untested, not old.
- Characterization tests pin today's behavior, including scars you are not ready to heal.
- Use production fixtures on the paths you will touch.
- Only after the pins are green should you tidy, then change behavior.
Further learning
Next, create a place to stand with seams.
Working Effectively with Legacy Code (Michael Feathers)In this book, Michael Feathers offers start-to-finish strategies for working more effectively with large, untested legacy code bases. This book draws on material Michael created for his renowned Object Mentor seminars…informit.comRelated
Refactoring and legacy code
Change code you did not write without breaking it. Start here for characterization, seams, tiny steps, and incremental migrations.
Testing strategy
What to test, at which level, and how to keep a suite fast, reliable, and worth the maintenance — from pyramid trade-offs to production safety nets.
Seams and dependency breaking
A seam is a place you can change behavior without editing the hard spot. Create one before you rewrite the algorithm.