Golden Master Testing: A Deep Dive
When you encounter a codebase with no tests and no documentation, how do you safely change it? You can't write traditional unit tests without understanding the expected behavior. You can't ask "what should this return?" because nobody knows—the code is the spec.
Golden master testing is the answer. It captures the current behavior of a system as the "golden master" (baseline), then detects any deviations from that baseline. It doesn't assert correct behavior—it asserts consistent behavior.
What Is a Golden Master?
The term comes from audio/film production: the golden master is the original reference recording from which copies are made. In testing, the golden master is the reference output against which all future runs are compared.
Golden master testing workflow:
- Run the code with representative inputs
- Capture the outputs—all of them, exactly as they are
- Store the outputs as the "golden master"
- On each future run, compare outputs to the golden master
- Any deviation is flagged as a change
The critical point: you don't know if the original output is correct. You're just establishing a baseline. Changes from that baseline are what you care about.
Why This Matters for Legacy Code
Consider a pricing calculation function that's been running in production for 8 years. Nobody knows all the edge cases it handles. The original author left. The spec document is outdated. But it's working—customers are paying the right prices.
You need to refactor it. How do you know you haven't broken anything?
Golden master approach:
# Step 1: Capture existing behavior for all inputs
test_inputs = load_production_inputs_sample() # real data from prod logs
golden_master = {}
for input_data in test_inputs:
output = legacy_pricing_function(input_data)
golden_master[hash(input_data)] = output
save_golden_master(golden_master)
# Step 2: Refactor the function
# Step 3: Verify your refactor produces identical output
for input_data in test_inputs:
new_output = refactored_pricing_function(input_data)
assert new_output == golden_master[hash(input_data)], \
f"Output changed for {input_data}"If the refactored function produces the same output for every input, you've preserved behavior—even if you don't know exactly what that behavior means for every case.
Golden Master vs. Approval Testing
These terms are used interchangeably, but there's a subtle distinction:
| Aspect | Golden Master Testing | Approval Testing |
|---|---|---|
| Origin | From characterization testing (Feathers) | From the ApprovalTests library |
| Typical use | Legacy code, large output sets, bulk comparison | Single-output review and approval |
| Approval process | Often automated (capture and store) | Human reviews and explicitly approves |
| Output size | Often large (many inputs → many outputs) | Usually one output per test |
| Tooling | Custom or general-purpose | Specialized libraries (ApprovalTests, etc.) |
In practice: golden master testing often refers to the strategy (capture existing behavior as baseline), while approval testing refers to the workflow (human-reviewed approval per test). Both use the same underlying mechanism.
Characterization Tests
Michael Feathers coined the term characterization tests in Working Effectively with Legacy Code—this is the predecessor to the "golden master" label.
A characterization test characterizes how a piece of code actually behaves, regardless of whether that behavior is correct:
@Test
void characterizeDiscountCalculation() {
// We don't know if 15.50 is "right"—we're capturing what it actually returns
assertEquals(15.50, legacyDiscountCalc.calculate(100.00, "GOLD", "US"), 0.01);
}You write these by running the code, observing the output, and committing those outputs as expected values. The test fails only if the behavior changes—not if the behavior is wrong.
Building a Golden Master for a Complex System
Step 1: Choose representative inputs
The quality of your golden master depends on input coverage. Use real production data when possible.
# Pull representative inputs from production logs
import json
import random
with open("prod_requests.log") as f:
all_requests = [json.loads(line) for line in f]
# Sample diverse inputs
sample = random.sample(all_requests, 1000) # 1000 representative casesStep 2: Capture outputs
golden_master = {}
for request in sample:
try:
output = legacy_system.process(request)
golden_master[request["id"]] = {
"input": request,
"output": serialize(output),
"captured_at": "2025-01-15"
}
except Exception as e:
golden_master[request["id"]] = {
"input": request,
"error": str(e),
"captured_at": "2025-01-15"
}
with open("golden_master.json", "w") as f:
json.dump(golden_master, f, indent=2)Step 3: Commit the golden master
git add golden_master.json
git commit -m "chore: capture golden master for legacy pricing system"Step 4: Run comparisons on every change
def test_output_matches_golden_master():
with open("golden_master.json") as f:
golden = json.load(f)
failures = []
for request_id, record in golden.items():
new_output = serialize(refactored_system.process(record["input"]))
if new_output != record["output"]:
failures.append({
"id": request_id,
"expected": record["output"],
"actual": new_output
})
assert not failures, f"{len(failures)} outputs changed:\n{json.dumps(failures[:5], indent=2)}"Handling Output Volatility
Like approval testing, golden master tests require deterministic output. Common problematic fields:
| Field | Problem | Solution |
|---|---|---|
| Timestamps | Change on every run | Freeze time or normalize to [timestamp] |
| Random IDs | Change on every run | Inject fixed values or scrub |
| Floating point | Precision varies by platform | Round to meaningful decimal places |
| Sort order | Unstable sorts vary | Sort output explicitly before capturing |
| Absolute paths | Machine-specific | Strip or relativize paths |
def normalize_output(output):
"""Make output deterministic for golden master comparison."""
output = re.sub(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z", "[timestamp]", output)
output = re.sub(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}", "[uuid]", output)
return outputWhen Golden Master Tests Reveal Bugs
The tricky part: the golden master captures current behavior, including bugs.
Suppose the legacy function returns wrong tax for certain states. Your golden master captures the wrong tax. Your refactored function passes the golden master test—and still returns wrong tax.
This is a known limitation. Golden master testing guarantees behavioral consistency, not correctness.
Handle it explicitly:
- Run the golden master to ensure refactoring doesn't change behavior
- Separately investigate and fix known bugs
- Update the golden master to reflect the fixed behavior
Treat bug fixes as intentional changes that require golden master updates.
Retiring Golden Masters
Golden masters are temporary scaffolding. Once you understand the code and have proper unit tests, phase them out:
- Write unit tests that specify correct behavior (including edge cases)
- Run both golden master and unit tests in parallel
- Verify unit tests catch everything the golden master would
- Remove the golden master
If you never retire them, golden masters become stale and expensive to maintain.
Tools for Golden Master Testing
| Language | Tool |
|---|---|
| Java | ApprovalTests.Java, TextTest |
| JavaScript/TypeScript | jest-snapshot, approval-tests |
| Python | approvaltests, pytest-snapshot |
| .NET | ApprovalTests.Net |
| C++ | Catch2 (with snapshot matchers) |
| Ruby | approvals gem |
Where End-to-End Tests Fit
Golden master tests verify internal outputs stay consistent. They don't cover the user-facing behavior. A function might produce the same JSON it always did, but if the UI that renders it breaks, users see a problem that golden master tests can't detect.
HelpMeTest runs end-to-end tests in plain English against your live application. When golden master tests confirm your refactored backend behaves consistently, HelpMeTest confirms that the frontend and API work together for real users.
Summary
- Golden master testing captures existing behavior as a reference (the "golden master")
- Changes from baseline trigger test failures—regardless of correctness
- Essential for safely refactoring legacy code with no tests
- Closely related to approval testing and characterization testing
- Requires deterministic output—normalize volatile fields before capturing
- Captures bugs along with correct behavior—don't confuse consistency with correctness
- Retire golden masters once you have proper unit tests