--- 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 패턴 |