| name | legacy-code-testing |
| description | Get untested, hard-to-change ("legacy") code safely under test using Michael Feathers's "Working Effectively with Legacy Code" — find seams, pin current behavior with characterization tests, and break dependencies so tangled code becomes testable. Use this skill whenever the user inherits or works with code that has no tests, is hard to instantiate or hard to run in a test, is tightly coupled, or "can't be tested without rewriting it"; whenever they need to change scary code safely, wrap a test harness around it, or break a dependency on a database, singleton, global, static, or `new` — even if they never mention the book or Feathers. Also for questions like "how do I test this untested class", "how do I break this dependency", "how do I change this safely when there are no tests", or "where do I even start with this legacy code". Enforces: legacy code = code without tests; pin actual behavior with characterization tests before changing; break dependencies at a seam (don't rewrite blind); and prefer Sprout/Wrap under time pressure. |
Working Effectively with Legacy Code (Feathers)
This skill makes you do what Michael Feathers's Working Effectively with Legacy Code
teaches: take code that has no tests and is scary to touch, and get it under test
so you can change it with confidence. It's the "I inherited a mess" skill — and the
on-ramp to every other testing skill in this repo, because those assume code you can
already test, and this is how you get there.
Operating definition: legacy code is code without tests. Not old code, not bad
code — untested code. Without tests you can't know whether a change broke something,
so every edit is a gamble. The whole method is about replacing that gamble with a
fast, mechanical safety net.
Examples are language-agnostic pseudocode; adapt to the user's stack and conventions.
The central dilemma
To change code safely you want tests around it. But to get tests around it you
often must change the code first (to break a dependency so you can instantiate or run
it). That's the legacy bind: you need to change code to test it, and you want tests
before you change code.
The resolution: make the initial dependency-breaking edits as conservative and
mechanical as possible — small, automatable, hard-to-get-wrong refactorings (the
catalogue in references/dependency-breaking-techniques.md) — so the window of
untested change is as safe as it can be. Then write tests, then make the real change
with the net in place.
The Legacy Code Change Algorithm
When you must change a legacy codebase, follow these five steps:
- Identify change points — where does the new behavior actually go?
- Find test points — where can you sense the effects of that change? Where will
you write the tests?
- Break dependencies — do the minimal, safe edits needed to get the code into a
test harness (instantiate the class, run the method) — at a seam, without altering
behavior.
- Write tests — usually characterization tests that pin the current behavior.
- Make changes and refactor — now make the real change and clean up, with the
tests catching any regression.
The day-to-day goal isn't "100% coverage." It's that every change leaves behind its
tests, so tested regions grow like islands until working in them is easy.
Seams and enabling points (the key idea)
A seam is a place where you can alter behavior without editing in that place.
Seams are how you slip a test double in front of an awkward dependency without
rewriting the code that uses it. Every seam has an enabling point — the place where
you choose which behavior is active.
Three kinds (full detail in references/seams-and-characterization-tests.md):
- Object Seam — change which method runs without changing the calling code, via
polymorphism: override a method in a Test-Specific Subclass, or inject a different
object. Enabling point: where the object/subclass is chosen (the constructor
parameter, the factory). The workhorse seam in OO code.
- Link Seam — substitute a different implementation at build/link time (a stub
library, a different binary or classpath). Enabling point: the build configuration /
classpath. Useful when there's no object seam (C/C++ libraries, hard static calls).
- Preprocessing Seam — replace calls before compilation (a macro/
#define).
Enabling point: the preprocessor. A last resort, mostly C/C++.
Finding the right seam is the central skill: it lets you control a dependency's inputs
and sense its outputs without the risky rewrite the dilemma seems to demand.
Characterization tests (the safety net)
When code has no tests and no written spec, you don't test what it should do — you
pin what it actually does right now, bugs and all, so that a refactor can't change
behavior unnoticed. That's a characterization test. The heuristic:
- Put the code in a test harness.
- Write an assertion you know will fail (e.g. assert the output equals something
absurd).
- Let the failure tell you the actual value the code produces.
- Change the assertion to expect that actual value.
- Repeat for more inputs, especially around interesting branches.
The point is documentation and safety, not correctness judgement: a characterization
test that encodes a current bug is doing its job — it tells you if you change that
behavior. Once the area is pinned, you can refactor and change freely with the net in
place. To choose which values to characterize, use coverage and effect sketches as a
guide (see references/seams-and-characterization-tests.md).
Then hand off to the four pillars. Characterization tests are a transitional
safety net, not the end state. Once code is pinned and you've made your change,
grow real behavior tests for the new/correct behavior using
khorikov-unit-testing (and derive their cases with effective-software-testing).
Where a characterization test encodes a bug you've now fixed, replace it with a
behavior test asserting the correct result.
Workflow — getting a class under test
- Try to instantiate the class in a test harness. Whatever stops you — a
constructor that hits a database, a global, a
new you can't control — is your
first dependency to break.
- Break that dependency at a seam, using the most conservative technique that
works (e.g. Extract Interface, Parameterize Constructor, Extract and
Override Factory Method, Subclass and Override Method). Pick by what's
blocking you — see the catalogue.
- Get the method to run in the harness; break whatever stops that (a static
call, a singleton — Introduce Static Setter, Extract and Override Call).
- Write characterization tests around the change point.
- Make the change (or, under pressure, Sprout/Wrap it — below) and refactor
with the net in place.
Don't try to break every dependency or test the whole class — break only what blocks
the change you need to make, and characterize only the area you're touching.
Under time pressure: Sprout and Wrap
When you can't yet get the surrounding code under test but must add behavior now,
add the new code in a tested, isolated piece instead of editing the untested mass:
- Sprout Method / Sprout Class — write the new behavior in a brand-new method (or
class), developed test-first, and call it from the existing code with a one-line
edit. The new code is tested even though its surroundings aren't.
- Wrap Method / Wrap Class — rename the old method and create a new one with the
old name that calls both the old behavior and your new behavior (or a Decorator class
that wraps the original). Adds behavior without modifying the original logic.
These confine the untested change to a single, obvious call site. They're a pragmatic
detour, not an excuse to skip getting the code under test later.
Reasoning tools: effect sketches and pinch points
- Effect sketches — a quick sketch of what affects what (which variables/outputs a
change can influence). They tell you where the effects of a change surface, hence
where to write tests. Simplifying the sketch is the goal of much of the refactoring.
- Pinch points — a narrow place through which many effects flow. Writing tests at a
pinch point lets a few tests cover a whole cluster of classes; it's a natural test
boundary while you work, to be tightened later.
Reference files
references/seams-and-characterization-tests.md — the three seam types and their
enabling points, and the full characterization-test procedure with how to pick values.
references/dependency-breaking-techniques.md — the complete 24-technique catalogue,
what each is for, and how to choose between them.
references/legacy-recipes.md — the "I need to…" playbook (the book's
question-titled scenarios), Sprout/Wrap steps, effect sketches, and pinch points.
../../EXAMPLES.md (repo root) has worked before/after pairs under "Working
Effectively with Legacy Code."