Testing Legacy & Untested Code
Software Testing Strategy
Chapter 9 · Testing Legacy & Untested Code
Clean Code, SOLID & Refactoring's own capstone refactored TangleMart's tangled order processor in six verified, dependency-ordered steps — but it never had to answer a harder question: what do you do when the code you need to touch has no tests at all yet? Michael Feathers, in Working Effectively with Legacy Code, gives legacy code a precise, testable definition: code without tests, regardless of its age or who wrote it. This chapter builds the technique for getting a safety net under code like that before changing a single line.
Discover, Don't Guess
(10, 'A', False) → 5.0, (10, 'B', False) → 6.0, (10, 'C', True) → 17.5, (-5, 'A', False) → 0.0 (negative weight silently clamped), (10, 'Z', False) → 10.0 — an unrecognized zone is charged more, not rejected as an error — and (60, 'A', False) → 27.0, reflecting an undocumented bulk discount past 50 units.
Characterization Tests as a Safety Net
A Safe Refactor, Verified Against the Same Net
calculate_shipping_refactored instead of the original: all 6 pass, including the exact same undocumented quirks — the negative-weight clamp, the unknown-zone default of 2.0, and the bulk discount. A genuinely cleaner implementation (dict lookup, no repeated if/elif), zero behavior change.
The Real Payoff: Catching an Accidental Change
calculate_shipping_WELLMEANING_FIX(10, 'Z', False) now returns 5.0 instead of the captured 10.0 — the characterization test fails. Whether 2.0 was a real bug or an intentional (if undocumented) business decision is still unknown — but the test forces that question to be asked and answered deliberately, instead of letting the change ship silently as a side effect of an unrelated refactor.
2.0 really is a bug, the fix is the same one shown above — but now it's a deliberate, reviewed change to the test's own expected value (exactly Chapter 8's own "update the scenario intentionally" pattern), not a silent side effect nobody noticed until a customer complained.
Where This Connects
| This chapter's finding | What it connects to |
|---|---|
| Capturing real behavior before refactoring, verified surviving a safe refactor | Clean Code, SOLID & Refactoring's own capstone — the missing first step for code that starts with zero tests |
| A test failing on an unintended change, forcing a deliberate decision | Chapter 8's own "update the scenario intentionally" pattern — the same discipline, applied to undocumented legacy behavior instead of a known business rule |
Hands-On Exercises
Probe this chapter's own legacy_calculate_shipping with a new input it wasn't tested against: (0, 'B', True) (zero weight, express). Discover the actual returned value by running the code, write a characterization test asserting it, and verify it passes against both the original and refactored versions.
This chapter's own bulk discount triggers at weight > 50. Discover what happens at exactly weight = 50 (the boundary itself) by running the code, and write a characterization test capturing that exact boundary behavior.
Introduce a second unintended change to this chapter's own refactored version: accidentally apply the bulk discount at weight >= 50 instead of weight > 50. Determine which of this chapter's own 6 characterization tests (if any) catches it, and explain why the answer depends on which specific inputs happen to already be covered.
Chapter 9 Quick Reference
- Feathers' definition: legacy code is simply code without tests — age and authorship don't matter
- Characterization tests: capture what the code actually does, discovered by running it — never what you think it should do
- Verified: 6 characterization tests captured a legacy function's real behavior, including 3 undocumented quirks (negative-weight clamping, an expensive unknown-zone default, a bulk discount)
- Verified: all 6 tests passed unchanged against a genuinely cleaner refactor, proving behavior was preserved exactly
- Verified: the same 6 tests caught a well-meaning "fix" that silently changed undocumented behavior — forcing a deliberate decision instead of an accidental one
- Next chapter: Capstone — designing a full test strategy for a real system, from the ground up