Golden Master Testing Strategy: Safe Refactoring with Characterization Tests

Golden Master Testing Strategy: Safe Refactoring with Characterization Tests

Working with legacy code is one of the most common and challenging software engineering tasks. The code works—mostly—but it's hard to understand, poorly documented, and changing it feels dangerous. You don't know what might break.

Golden master testing (also called characterization testing) solves this by capturing the current behavior of the system before you change anything. Whatever the code produces becomes the "golden master"—the benchmark against which all future runs are compared. If your refactoring changes the output, you know immediately.

What Is a Golden Master?

A golden master is the captured output of a system at a specific point in time. It's called a "master" because it becomes the reference—the authoritative answer to "what should this code produce?"

The process is:

  1. Run the system with representative inputs
  2. Capture all outputs (return values, side effects, logs)
  3. Save these as your golden master
  4. Refactor freely—any behavioral change breaks the test
  5. Review failures: regression or intentional change?

The power is that you don't need to understand the code to create a golden master. You just need to run it and save what comes out.

When to Use Golden Master Testing

Legacy code refactoring: You're cleaning up old code but can't risk changing behavior.

Adding features to untested code: Before adding new functionality, characterize existing behavior.

Performance optimization: You want to speed up code without changing its output.

Technology migrations: Rewriting from Python 2 to 3, or from one ORM to another.

Third-party API changes: Capturing current API response structure before an API version upgrade.

Creating a Golden Master in Practice

Python Example

# tests/golden_master/test_report_generator.py
import json
import pytest
from pathlib import Path
from myapp.reports import ReportGenerator

GOLDEN_MASTER_DIR = Path(__file__).parent / "golden_files"
GOLDEN_MASTER_DIR.mkdir(exist_ok=True)

class TestReportGeneratorGoldenMaster:
    """
    Golden master tests for ReportGenerator.
    
    These tests capture current behavior. When refactoring,
    any change to output will fail these tests—helping you 
    catch unintended behavioral changes.
    """
    
    def test_monthly_report(self, db_with_test_data):
        generator = ReportGenerator(db_with_test_data)
        result = generator.generate_monthly_report(month="2024-01")
        
        golden_file = GOLDEN_MASTER_DIR / "monthly_report_jan_2024.txt"
        
        if not golden_file.exists():
            # First run: create the golden master
            golden_file.write_text(result)
            pytest.skip(f"Golden master created at {golden_file}. Review and re-run.")
        
        expected = golden_file.read_text()
        assert result == expected, \
            f"Output differs from golden master. Review the diff:\n" \
            f"Expected:\n{expected}\n\nActual:\n{result}"
    
    def test_quarterly_summary(self, db_with_test_data):
        generator = ReportGenerator(db_with_test_data)
        result = generator.generate_quarterly_summary(quarter="Q1-2024")
        
        golden_file = GOLDEN_MASTER_DIR / "quarterly_summary_q1_2024.json"
        
        if not golden_file.exists():
            golden_file.write_text(json.dumps(result, indent=2))
            pytest.skip(f"Golden master created. Review {golden_file}")
        
        expected = json.loads(golden_file.read_text())
        assert result == expected

Using ApprovalTests Library

ApprovalTests automates the golden master pattern:

from approvaltests import verify, verify_as_json

def test_legacy_customer_formatter():
    """
    Characterization test for the legacy CustomerFormatter class.
    Output may not be ideal, but we're capturing it before refactoring.
    """
    formatter = LegacyCustomerFormatter()
    customer = Customer(
        name="Jane Smith",
        email="jane@example.com",
        tier="gold",
        join_date="2020-03-15"
    )
    
    result = formatter.format(customer)
    verify(result)  # Compares to .approved.txt file

Handling Side Effects

Golden master tests must capture all observable behavior, including side effects:

import io
from unittest.mock import patch

def test_legacy_report_with_side_effects():
    """Capture output including print statements and file writes."""
    
    captured_stdout = io.StringIO()
    written_files = {}
    
    def mock_write(filename, content):
        written_files[filename] = content
    
    with patch('sys.stdout', captured_stdout), \
         patch('builtins.open', create=True) as mock_open:
        
        # Set up file write capture
        mock_open.side_effect = lambda f, mode='r': MockFile(f, written_files)
        
        # Run the legacy code
        legacy_report_generator()
    
    # Capture everything
    golden_output = {
        "stdout": captured_stdout.getvalue(),
        "files_written": written_files
    }
    
    verify(json.dumps(golden_output, indent=2))

Managing Non-Deterministic Output

Legacy code often produces non-deterministic output: timestamps, random IDs, session tokens. You need to make this deterministic before creating a golden master.

def make_output_deterministic(output: str) -> str:
    """Replace non-deterministic values with stable placeholders."""
    import re
    
    # Timestamps
    output = re.sub(
        r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?Z?',
        '<TIMESTAMP>',
        output
    )
    
    # 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
    )
    
    # Sequential IDs (assuming they increment)
    id_counter = [0]
    def replace_id(match):
        id_counter[0] += 1
        return f'<ID_{id_counter[0]}>'
    
    output = re.sub(r'"id"\s*:\s*\d+', replace_id, output)
    
    return output

def test_legacy_system_output():
    result = run_legacy_system()
    stable_result = make_output_deterministic(result)
    verify(stable_result)

Golden Master for Database State

Don't just test the return value—capture database state after operations:

def test_order_processing_database_state():
    """Golden master for database state after order processing."""
    db = setup_test_database()
    order = create_test_order()
    
    # Run the legacy order processing code
    process_order(order.id, db)
    
    # Capture all affected tables
    state = {
        "orders": db.execute("SELECT * FROM orders WHERE id = ?", (order.id,)).fetchall(),
        "order_items": db.execute("SELECT * FROM order_items WHERE order_id = ?", (order.id,)).fetchall(),
        "inventory": db.execute("SELECT sku, quantity FROM inventory ORDER BY sku").fetchall(),
        "audit_log": db.execute("SELECT action FROM audit_log ORDER BY created_at").fetchall()
    }
    
    # Make deterministic
    state_str = json.dumps(state, indent=2, default=str)
    state_str = make_output_deterministic(state_str)
    
    verify(state_str)

Using Golden Masters to Guide Refactoring

The golden master workflow for safe refactoring:

# Step 1: Create golden master (BEFORE any refactoring)
def test_legacy_calculation_golden_master():
    inputs = generate_representative_inputs()  # Cover edge cases
    
    results = {}
    for input_data in inputs:
        results[str(input_data)] = legacy_calculation(input_data)
    
    verify(json.dumps(results, indent=2))

# Step 2: Refactor the code (golden master protects you)
def legacy_calculation(n):
    # Original: complex, hard to understand
    result = 0
    for i in range(n):
        if i % 2 == 0:
            result += i * i
        else:
            result -= i
    return result

# Becomes:
def refactored_calculation(n):
    # Clearer implementation
    even_squares = sum(i*i for i in range(n) if i % 2 == 0)
    odd_sum = sum(i for i in range(n) if i % 2 != 0)
    return even_squares - odd_sum

# Step 3: Update tests to use refactored function
# The golden master ensures refactored_calculation == legacy_calculation

Incrementally Improving Golden Masters

Once you have a golden master, you can start improving the output quality:

# Original golden master captures buggy output
# "Total: $1000.0" (formatting bug: should be $1,000.00)

# After fixing the bug:
# 1. Test fails (output changed)
# 2. Review the diff - the change is intentional (bug fix)
# 3. Approve the new output by updating the golden master file
# 4. Continue

# This makes bug fixes explicit and reviewed

Golden Master Testing Checklist

Setting up golden masters:

  • Representative test data covering normal cases, edge cases, error cases
  • Non-deterministic values (timestamps, IDs) are scrubbed or controlled
  • All observable behavior captured (return value, database state, side effects)
  • Golden master files committed to source control

Using golden masters during refactoring:

  • All golden master tests pass before starting refactoring
  • Run tests after every significant change
  • Review all failures: regression or intentional?
  • Update golden masters only for intentional changes, with documented reasons
  • Run full suite before merging

Conclusion

Golden master testing is the safety net that makes refactoring legacy code manageable. Without tests, refactoring is risky guesswork. With golden masters, you can refactor confidently—any behavioral change is caught immediately.

The technique is especially powerful because it doesn't require understanding the code. You just need to run it, capture the output, and let the test framework guard against changes.

Start your next legacy refactoring project by creating golden masters first. Spend an hour capturing current behavior, and you'll save days of debugging—or prevent that production incident entirely.

Read more

Start now free