---
id: wiki-2026-0508-characterization-tests-특성화-테스트
title: Characterization Tests (특성화 테스트)
category: 10_Wiki/Topics
status: verified
canonical_id: self
aliases: [Golden Master Tests, Approval Tests, Pinning Tests, Snapshot Tests]
duplicate_of: none
source_trust_level: A
confidence_score: 0.9
verification_status: applied
tags: [testing, legacy-code, feathers, refactoring]
raw_sources: []
last_reinforced: 2026-05-10
github_commit: pending
tech_stack:
  language: typescript-python
  framework: jest-pytest-approvaltests
---

# Characterization Tests (특성화 테스트)

## 매 한 줄
> **"매 legacy code 의 actual behavior 의 capture — 매 spec 이 X, 매 photo 가 O."**. Michael Feathers 의 *Working Effectively with Legacy Code* (2004) 에서 정립. 매 untested legacy 의 refactoring 시작점. 매 "what should it do" X — 매 "what does it do right now" 의 lock down. 2026년 snapshot tests, ApprovalTests, golden master tests 모두 같은 family.

## 매 핵심

### 매 정의 (Feathers)
> "A characterization test is a test that characterizes the actual behavior of a piece of code. There's no 'correct'... it just records what the system does."

### 매 vs unit/spec test
| 측면 | Spec Test | Characterization Test |
|---|---|---|
| Source of truth | Requirements / spec | Actual current behavior |
| Failing means | Code has bug | Behavior changed (maybe intended) |
| When to write | TDD, before code | Before refactoring legacy code |
| Update on change | Fix code | Review diff, accept if intended |

### 매 procedure (Feathers)
1. Pick code 의 region.
2. Write test 가 invokes the code with realistic inputs.
3. Assert with **placeholder** (e.g. `assert result == "FILL_ME"`).
4. Run, capture actual output.
5. Replace placeholder with actual output.
6. Now test pins behavior — refactor with confidence.

### 매 variants
- **Snapshot test** (Jest, Vitest): serialize output, compare next run.
- **Approval test** (ApprovalTests): write to `.approved.txt`, manual review on diff.
- **Golden master**: large input/output pair, often UI screenshot.
- **Property-based regression**: random inputs, save outputs as golden.

### 매 응용
1. Refactoring legacy monolith without specs.
2. Migration (framework upgrade, language port).
3. Compiler / parser output stability.
4. Report generation (PDFs, CSVs).
5. UI visual regression (Percy, Chromatic).

## 💻 패턴

### Feathers procedure (TypeScript + Jest)
```typescript
import { calculateInvoice } from './legacy';

describe('calculateInvoice — characterization', () => {
  it('records current behavior for typical input', () => {
    const result = calculateInvoice({
      items: [
        { sku: 'A', qty: 2, price: 19.99 },
        { sku: 'B', qty: 1, price: 5.00 },
      ],
      customerType: 'premium',
      country: 'KR',
    });

    // Step 1: write `expect(result).toBe('PLACEHOLDER')`
    // Step 2: run, observe actual:
    //   { subtotal: 44.98, discount: 4.50, vat: 4.05, total: 44.53 }
    // Step 3: paste it as expected
    expect(result).toEqual({
      subtotal: 44.98,
      discount: 4.50,
      vat: 4.05,
      total: 44.53,
    });
  });
});
```

### Jest snapshot
```typescript
import { renderInvoiceHtml } from './render';

test('invoice html — characterized', () => {
  const html = renderInvoiceHtml(SAMPLE_INVOICE);
  expect(html).toMatchSnapshot();
});
// First run: writes __snapshots__/invoice.test.ts.snap
// Future runs: diffs. Use `--update-snapshot` after intentional change.
```

### ApprovalTests (Python)
```python
from approvaltests import verify

def test_pdf_layout():
    pdf_text = render_pdf(sample_data)
    verify(pdf_text)
# Writes `test_pdf_layout.received.txt`, compares to `.approved.txt`.
# CI fails on diff; dev reviews then renames received→approved.
```

### Golden master with multiple inputs
```python
import json
from pathlib import Path
from legacy_pricing import compute_price

def test_pricing_golden_master():
    cases = json.loads(Path("fixtures/cases.json").read_text())
    actual = [compute_price(c["input"]) for c in cases]
    expected = json.loads(Path("fixtures/expected.json").read_text())
    assert actual == expected
```

### Generating the golden initially
```python
# tools/regenerate_golden.py — run once, then commit
import json
from pathlib import Path
from legacy_pricing import compute_price

cases = json.loads(Path("fixtures/cases.json").read_text())
out = [compute_price(c["input"]) for c in cases]
Path("fixtures/expected.json").write_text(json.dumps(out, indent=2))
```

### Differential testing (old vs refactored)
```typescript
import { computePriceLegacy } from './pricing-legacy';
import { computePriceNew } from './pricing-new';
import fc from 'fast-check';

test('refactor preserves behavior', () => {
  fc.assert(
    fc.property(
      fc.record({
        qty: fc.integer({ min: 1, max: 100 }),
        unitPrice: fc.float({ min: 0.01, max: 9999 }),
      }),
      (input) => {
        expect(computePriceNew(input))
          .toBeCloseTo(computePriceLegacy(input), 2);
      },
    ),
    { numRuns: 1000 },
  );
});
```

### Capture I/O of legacy via instrumentation
```python
# Wrap legacy fn, log all inputs+outputs in production for a week,
# then replay as test fixtures
from functools import wraps
import json, time

def record(path):
    def deco(fn):
        @wraps(fn)
        def wrapper(*args, **kwargs):
            result = fn(*args, **kwargs)
            with open(path, "a") as f:
                f.write(json.dumps({
                    "ts": time.time(),
                    "args": args, "kwargs": kwargs,
                    "result": result,
                }, default=str) + "\n")
            return result
        return wrapper
    return deco

@record("fixtures/legacy_calls.jsonl")
def legacy_compute(...): ...
```

## 매 결정 기준
| 상황 | Approach |
|---|---|
| Pure function legacy | Inline assertion (Feathers procedure) |
| Large structured output | Snapshot or ApprovalTests |
| Visual UI | Storybook + Chromatic / Percy |
| Two impls (refactor) | Differential testing + property-based |
| Production behavior unknown | Record-replay from instrumentation |

**기본값**: Snapshot test for serializable output, ApprovalTests for human-reviewed diffs (PDF, HTML), differential test during refactor.

## 🔗 Graph
- 부모: [[Software Testing]] · [[Legacy Code]]
- 변형: [[Snapshot Testing]] · [[Approval Tests]] · [[Golden Master]]
- 응용: [[Refactoring]] · [[Visual Regression]] · [[Migration Testing]]
- Adjacent: [[Property-Based Testing]] · [[TDD]] · [[Test Doubles]]

## 🤖 LLM 활용
**언제**: legacy code refactor 시작, untested codebase 첫 test net, framework migration, output-shape stability (PDF/CSV/JSON).
**언제 X**: greenfield TDD (use spec tests), rapidly evolving design (snapshots churn), security-critical (need real spec).

## ❌ 안티패턴
- **Treating snapshot as spec**: 매 snapshot fail — auto-update without review. 매 bug 의 silent merge.
- **Huge unreadable snapshot**: 1000-line JSON — split into focused snapshots.
- **No fixture review process**: golden master changes auto-merge — require reviewer.
- **Characterization without refactor goal**: tests forever pin "legacy bug" 의 behavior — note bugs explicitly, fix later.
- **Time/random in capture**: nondeterministic snapshots — freeze clock/seed.

## 🧪 검증 / 중복
- Verified (Feathers "Working Effectively with Legacy Code" 2004 / Jest docs / ApprovalTests / Beck "Test Driven Development").
- 신뢰도 A.

## 🕓 Changelog
| 날짜 | 변경 |
|---|---|
| 2026-05-08 | Phase 1 |
| 2026-05-10 | Manual cleanup — Feathers procedure + snapshot/approval/differential 패턴 |
