"매 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)
Pick code 의 region.
Write test 가 invokes the code with realistic inputs.
Assert with placeholder (e.g. assert result == "FILL_ME").
Run, capture actual output.
Replace placeholder with actual output.
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.
import{renderInvoiceHtml}from'./render';test('invoice html — characterized',()=>{consthtml=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)
fromapprovaltestsimportverifydeftest_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.
# tools/regenerate_golden.py — run once, then commitimportjsonfrompathlibimportPathfromlegacy_pricingimportcompute_pricecases=json.loads(Path("fixtures/cases.json").read_text())out=[compute_price(c["input"])forcincases]Path("fixtures/expected.json").write_text(json.dumps(out,indent=2))
# Wrap legacy fn, log all inputs+outputs in production for a week,# then replay as test fixturesfromfunctoolsimportwrapsimportjson,timedefrecord(path):defdeco(fn):@wraps(fn)defwrapper(*args,**kwargs):result=fn(*args,**kwargs)withopen(path,"a")asf:f.write(json.dumps({"ts":time.time(),"args":args,"kwargs":kwargs,"result":result,},default=str)+"\n")returnresultreturnwrapperreturndeco@record("fixtures/legacy_calls.jsonl")deflegacy_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.