Golden Master Testing
How to use golden master testing to characterize legacy code behavior before refactoring. Covers capturing output, comparing baselines, and incremental refactoring.
Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.
Overview
Golden master testing (also called characterization testing) captures the current behavior of a system as a baseline, then verifies that future changes don’t alter that behavior. It’s the primary technique for testing legacy code that has no tests — you don’t assert what the code should do, you assert what it currently does. The name comes from the “golden master” — a reference output that serves as the truth. When you refactor, any deviation from the golden master signals a regression.
When to Use
- Legacy code with no tests that you need to refactor safely
- Code where the business rules are unclear or undocumented
- Systems where the output is deterministic but complex (reports, calculations, transformations)
- Before migrating a system to a new language or framework
- When you need confidence to change code you don’t fully understand
When NOT to Use
- New code with clear requirements — write tests first (TDD)
- When output is non-deterministic — random values, timestamps, network calls
- For simple functions — explicit assertions are clearer
- When you understand the code well enough to write targeted tests
- For code that is about to be replaced entirely — don’t characterize what you’ll delete
Solution
Basic golden master approach
# Python — capture output as golden master
import json
import subprocess
import hashlib
def run_legacy_system(inputs):
"""Run the legacy system with given inputs and capture output."""
result = subprocess.run(
['python', 'legacy_calculator.py'],
input=json.dumps(inputs),
capture_output=True,
text=True,
)
return result.stdout
def generate_golden_master(input_file):
"""Generate golden master from input file."""
with open(input_file) as f:
inputs = json.load(f)
outputs = []
for input_data in inputs:
output = run_legacy_system(input_data)
outputs.append({
'input': input_data,
'output': output,
'hash': hashlib.sha256(output.encode()).hexdigest(),
})
with open('golden_master.json', 'w') as f:
json.dump(outputs, f, indent=2)
print(f"Golden master generated with {len(outputs)} cases")
# Step 1: Generate golden master
generate_golden_master('test_inputs.json')
Comparing against golden master
# Python — verify refactored code matches golden master
import json
import subprocess
import hashlib
import pytest
def load_golden_master():
with open('golden_master.json') as f:
return json.load(f)
def run_refactored_system(input_data):
"""Run the refactored system."""
result = subprocess.run(
['python', 'refactored_calculator.py'],
input=json.dumps(input_data),
capture_output=True,
text=True,
)
return result.stdout
@pytest.mark.parametrize("case", load_golden_master())
def test_matches_golden_master(case):
input_data = case['input']
expected_output = case['output']
expected_hash = case['hash']
actual_output = run_refactored_system(input_data)
actual_hash = hashlib.sha256(actual_output.encode()).hexdigest()
assert actual_hash == expected_hash, (
f"Output mismatch for input: {input_data}\n"
f"Expected: {expected_output[:200]}\n"
f"Actual: {actual_output[:200]}"
)
Golden master with file-based output
# Python — compare file output (e.g., generated reports)
import os
import filecmp
import subprocess
import pytest
def generate_report(input_file, output_dir, system='legacy'):
"""Generate a report using either legacy or refactored system."""
script = f'{system}_report_generator.py'
subprocess.run([
'python', script,
'--input', input_file,
'--output', output_dir,
], check=True)
@pytest.fixture(scope="session")
def golden_master_reports(tmp_path_factory):
"""Generate golden master reports once per session."""
golden_dir = tmp_path_factory.mktemp("golden")
test_inputs = [
'tests/data/report_input_1.json',
'tests/data/report_input_2.json',
'tests/data/report_input_3.json',
]
for i, input_file in enumerate(test_inputs):
output_file = golden_dir / f'report_{i}.html'
generate_report(input_file, str(output_file), system='legacy')
return golden_dir, test_inputs
def test_refactored_matches_golden(golden_master_reports, tmp_path):
golden_dir, test_inputs = golden_master_reports
for i, input_file in enumerate(test_inputs):
# Generate with refactored system
refactored_output = tmp_path / f'report_{i}.html'
generate_report(input_file, str(refactored_output), system='refactored')
# Compare
golden_file = golden_dir / f'report_{i}.html'
assert filecmp.cmp(golden_file, refactored_output, shallow=False), (
f"Report mismatch for {input_file}"
)
Golden master with large input sets
# Python — generate large input sets for thorough characterization
import random
import json
def generate_test_inputs(count=1000):
"""Generate diverse inputs to exercise the legacy system."""
inputs = []
# Normal cases
for i in range(count // 2):
inputs.append({
'operation': random.choice(['add', 'subtract', 'multiply', 'divide']),
'a': random.uniform(-1000, 1000),
'b': random.uniform(-1000, 1000),
})
# Edge cases
inputs.extend([
{'operation': 'add', 'a': 0, 'b': 0},
{'operation': 'divide', 'a': 1, 'b': 0}, # Division by zero
{'operation': 'multiply', 'a': -1, 'b': -1},
{'operation': 'add', 'a': 1e10, 'b': -1e10},
{'operation': 'subtract', 'a': 0.1, 'b': 0.3},
])
# Boundary values
inputs.extend([
{'operation': 'add', 'a': float('inf'), 'b': 1},
{'operation': 'add', 'a': float('-inf'), 'b': 1},
{'operation': 'add', 'a': float('nan'), 'b': 1},
])
with open('test_inputs.json', 'w') as f:
json.dump(inputs, f, indent=2)
print(f"Generated {len(inputs)} test inputs")
Approval testing approach
# Python — approval testing with approvaltests library
from approvaltests import Approvals
from approvaltests.reporters import PythonNativeReporter
def test_legacy_formatter_output():
# Run the legacy formatter with test input
input_data = [
{"name": "Alice", "age": 30, "role": "admin"},
{"name": "Bob", "age": 25, "role": "member"},
{"name": "Charlie", "age": 35, "role": "guest"},
]
output = legacy_formatter.format_table(input_data)
# First run: creates approved file (golden master)
# Subsequent runs: compares against approved file
# If mismatch: opens diff tool for review
Approvals.verify(output, reporter=PythonNativeReporter())
def test_legacy_calculator_with_multiple_inputs():
results = []
for a, b, op in [(1, 2, 'add'), (10, 5, 'subtract'), (3, 4, 'multiply')]:
result = legacy_calculator.calculate(a, b, op)
results.append(f"{a} {op} {b} = {result}")
output = '\n'.join(results)
Approvals.verify(output)
JavaScript golden master
// JavaScript — golden master with Jest snapshot
const { execSync } = require('child_process');
function runSystem(input, system = 'legacy') {
const result = execSync(`node ${system}_calculator.js`, {
input: JSON.stringify(input),
encoding: 'utf-8',
});
return result;
}
describe('Golden Master: legacy vs refactored', () => {
const testCases = require('./test_inputs.json');
test.each(testCases)('matches golden master for %j', (input) => {
const legacyOutput = runSystem(input, 'legacy');
const refactoredOutput = runSystem(input, 'refactored');
expect(refactoredOutput).toBe(legacyOutput);
});
});
// Alternative: use Jest snapshots as golden master
describe('Golden Master with snapshots', () => {
const testCases = require('./test_inputs.json');
test.each(testCases)('output matches snapshot for %j', (input) => {
const output = runSystem(input, 'legacy');
expect(output).toMatchSnapshot();
});
});
Incremental refactoring with golden master
# Python — step-by-step refactoring with golden master safety net
# Step 1: Generate golden master (before any changes)
# python generate_golden_master.py
# Step 2: Write characterization tests
class TestCharacterization:
@pytest.mark.parametrize("case", load_golden_master())
def test_legacy_behavior_preserved(self, case):
output = run_current_system(case['input'])
assert output == case['output']
# Step 3: Refactor in small steps, running tests after each change
# - Extract method
# - Rename variable
# - Replace conditional with polymorphism
# - Each step: run golden master tests
# Step 4: Once refactored, write proper unit tests for the new structure
# - Delete golden master tests once unit tests provide equal confidence
Handling non-deterministic output
# Python — normalize non-deterministic values before comparison
import re
from datetime import datetime
def normalize_output(output):
"""Remove non-deterministic elements from output."""
# Replace timestamps
output = re.sub(
r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.\d+Z?',
'<TIMESTAMP>',
output,
)
# Replace UUIDs
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,
)
# Replace random IDs
output = re.sub(r'"id": \d+', '"id": <ID>', output)
# Replace file paths
output = re.sub(r'/tmp/[^\s"]+', '<TMP_PATH>', output)
return output
def test_with_normalization():
input_data = {'action': 'create', 'name': 'Alice'}
legacy_output = normalize_output(run_legacy_system(input_data))
refactored_output = normalize_output(run_refactored_system(input_data))
assert refactored_output == legacy_output
Variants
Parallel run in production
# Python — run legacy and refactored in parallel, compare results
import logging
def parallel_run(input_data):
"""Run both systems and log differences."""
try:
legacy_result = run_legacy_system(input_data)
refactored_result = run_refactored_system(input_data)
if legacy_result != refactored_result:
logging.warning(
f"Output mismatch detected!\n"
f"Input: {input_data}\n"
f"Legacy: {legacy_result[:200]}\n"
f"Refactored: {refactored_result[:200]}"
)
return False
return True
except Exception as e:
logging.error(f"Error in parallel run: {e}")
return False
# Run in production with shadow traffic
# Log mismatches but serve from legacy system
Golden master with database state
# Python — capture database state as golden master
import json
import hashlib
def capture_db_state(db_connection):
"""Capture current database state as a hash."""
tables = ['users', 'orders', 'products']
state = {}
for table in tables:
rows = db_connection.execute(f'SELECT * FROM {table} ORDER BY id')
state[table] = [dict(row) for row in rows]
state_json = json.dumps(state, sort_keys=True, default=str)
return hashlib.sha256(state_json.encode()).hexdigest()
def test_database_state_unchanged_after_refactor():
# Setup: populate database with test data
setup_test_data(db)
# Capture golden master state
golden_hash = capture_db_state(db)
# Run the refactored operation
refactored_service.process_orders()
# Verify database state matches
current_hash = capture_db_state(db)
assert current_hash == golden_hash, "Database state changed unexpectedly"
Golden master with API responses
// JavaScript — capture API responses as golden master
const axios = require('axios');
async function captureApiResponses(baseUrl) {
const endpoints = [
{ method: 'GET', path: '/api/users' },
{ method: 'GET', path: '/api/users/1' },
{ method: 'GET', path: '/api/orders' },
{ method: 'POST', path: '/api/orders', body: { userId: 1, total: 100 } },
];
const responses = [];
for (const endpoint of endpoints) {
const res = await axios({
method: endpoint.method,
url: `${baseUrl}${endpoint.path}`,
data: endpoint.body,
});
responses.push({
endpoint,
status: res.status,
body: res.data,
});
}
return responses;
}
test('refactored API matches golden master', async () => {
const goldenMaster = require('./api_golden_master.json');
const refactoredResponses = await captureApiResponses('http://localhost:3001');
expect(refactoredResponses).toEqual(goldenMaster);
});
Best Practices
-
For a deeper guide, see Contract Testing: Verify Consumer-Producer API Contracts.
-
Generate diverse inputs — normal, edge, boundary, and invalid cases
-
Normalize non-deterministic values — timestamps, UUIDs, random IDs before comparison
-
Use hashes for large outputs — SHA-256 comparison is faster than string comparison
-
Run golden master tests in CI — catch regressions early
-
Keep the golden master under version control — track what behavior you characterized
-
Refactor in small steps — run golden master after each change
-
Delete golden master tests when done — once you have proper unit tests, the golden master is redundant
-
Document what the golden master covers — future developers need to know what behavior is characterized
Common Mistakes
- Insufficient test inputs: generating only happy-path inputs misses edge cases. Include boundary and invalid inputs.
- Not normalizing output: timestamps and random values cause false failures. Always normalize before comparison.
- Updating golden master blindly: when the golden master changes, review the diff carefully. A change might be a regression, not an improvement.
- Keeping golden master forever: golden master tests are a safety net for refactoring. Once you have proper tests, delete them.
- Testing too much output: comparing entire HTML pages or large JSON makes diffs unreadable. Focus on the relevant parts.
FAQ
What is a golden master test?
A test that captures the current output of a system as a baseline (the “golden master”) and verifies that future changes produce the same output. It characterizes what the code does, not what it should do.
How is this different from snapshot testing?
Snapshot testing is a specific implementation of golden master testing. Golden master is the concept; snapshots are one tool. Other tools include file comparison, hash comparison, and approval testing.
When should I delete golden master tests?
When you have proper unit tests that provide equal or better confidence. Golden master tests are a temporary safety net for refactoring legacy code, not a permanent testing strategy.
What if the golden master has a bug?
The golden master captures current behavior, including bugs. If you find a bug, fix it and update the golden master. Document that the behavior changed intentionally.
How many test inputs do I need?
Enough to exercise all code paths. Start with 100-1000 inputs covering normal, edge, and boundary cases. For complex systems, use property-based testing to generate inputs automatically.
Related Resources
Snapshot Testing: Capture and Compare Serialized Output
How to use snapshot testing to detect unintended changes in serialized output. Covers Jest snapshots, pytest snapshot, and inline vs external snapshots.
PatternTest Double: Replace Dependencies with Stubs, Spies, Fakes
How to use test doubles to isolate units under test. Covers stubs, spies, fakes, mocks, and dummy objects with examples in Python, JavaScript, and Java.
PatternTest Pyramid: Balance Unit, Integration
How to structure a test suite using the test pyramid. Covers unit, integration, and E2E test proportions, the testing trophy, and ice cream cone anti-pattern.