Golden Master Testing: Safely Refactoring Legacy Code Without Specs
You can't write example-based tests for behavior nobody can fully describe. Golden master (approval) testing captures what the code actually does today, so a refactor can be verified without ever writing down the spec.
Refactoring a legacy function nobody fully understands has a chicken-and-egg problem: you want tests before you refactor, but writing tests requires knowing the correct expected output, and if anyone fully knew that, the code probably wouldn't be considered risky legacy code in the first place. Golden master testing sidesteps this: instead of asserting what the output *should* be, it captures what the output *currently is*, and fails if a change alters it.
How it works
- Run the existing code against a wide range of realistic inputs and record the actual output for each — this recorded output is the "golden master."
- Commit the golden master files alongside the test suite.
- Any future change re-runs the same inputs and diffs the new output against the golden master.
- A diff means behavior changed — either an unintended regression (fix the code) or an intended change (review the diff, then approve and update the golden master).
Golden master tests give you a safety net for refactoring — confidence that you haven't changed what the code does. They don't validate that what it does was right in the first place. That's a separate, harder problem outside the scope of this technique.
Where AI actually helps
The hard part of golden master testing isn't the mechanism (any approval-testing library handles the recording and diffing) — it's choosing inputs that exercise enough of the code's actual behavior to make the safety net meaningful. A golden master built from three happy-path inputs catches almost nothing; one built from a systematically varied input set catches real regressions.
Context: Here is a legacy pricing calculation function nobody currently on the team fully understands. Here is its signature and the code: [paste code]
Task: Generate a list of 15-20 representative input combinations to use for golden master testing, before any refactor begins.
Constraints:
- Cover boundary values for every numeric parameter (zero, negative if the type allows it, very large values), not just typical mid-range values.
- Cover every branch visible in the code (each if/else/switch path should be hit by at least one input).
- Include combinations that look like they might trigger unusual interactions between parameters, based on the code's structure — flag which inputs you chose specifically for this reason and why.
- Do not attempt to predict what the output *should* be for each input — the golden master process records the actual current output, it doesn't need a predicted one.Golden master vs. characterization tests vs. real unit tests
| Technique | What it asserts | When to use it |
|---|---|---|
| Golden master / approval test | Output for a given input is unchanged from a recorded snapshot | Before refactoring legacy code with unclear intended behavior — a temporary safety net |
| Characterization test | A specific, hand-picked example of current behavior, documented and asserted explicitly (a narrower, human-curated version of the same idea) | Documenting specific known behaviors worth calling out by name, not blanket coverage |
| Real unit test | Output matches a known-correct expected value, derived from the actual specification | Once behavior is understood well enough to state what's correct, not just what's current — the end goal, not the starting point |
The intended lifecycle: golden master tests go in first as a temporary net, the refactor proceeds under their protection, and as understanding of the correct behavior improves during the refactor, some golden master tests get replaced by real unit tests that assert actual correctness rather than mere consistency. Leaving golden master tests as the permanent test suite long-term means you've locked in "whatever it currently does" as the spec — including any bugs.