--- id: testing-property-based title: Property-based Testing — fast-check / 자동 input category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [testing, property-based, fast-check, vibe-coding] tech_stack: { language: "TS / fast-check", applicable_to: ["Backend", "Frontend"] } applied_in: [] aliases: [property-based testing, fast-check, QuickCheck, Hypothesis, generator, shrinking] --- # Property-based Testing > 예시 input 만 테스트 = edge case 놓침. **속성 (property) + 자동 input 생성** = 100+ random case 시도. fast-check (TS), Hypothesis (Python), QuickCheck (Haskell). ## 📖 핵심 개념 - Property: "모든 input X 에 대해 결과 Y" 같은 universal 진리. - Generator: random input 자동 생성. - Shrinking: 실패한 input → 최소 reproduce. ## 💻 코드 패턴 ### fast-check 기본 ```ts import fc from 'fast-check'; import { describe, test, expect } from 'vitest'; function reverse(arr: T[]): T[] { return [...arr].reverse(); } describe('reverse', () => { test('reverse twice = original', () => { fc.assert( fc.property(fc.array(fc.integer()), (arr) => { expect(reverse(reverse(arr))).toEqual(arr); }) ); }); test('length preserved', () => { fc.assert( fc.property(fc.array(fc.anything()), (arr) => { expect(reverse(arr).length).toBe(arr.length); }) ); }); }); ``` `fc.assert` 가 100번 (default) random run. ### Generators ```ts fc.integer({ min: 0, max: 100 }) fc.float({ min: 0, max: 1, noNaN: true }) fc.string({ minLength: 1, maxLength: 100 }) fc.array(fc.integer(), { minLength: 1 }) fc.uuid() fc.emailAddress() fc.date({ min: new Date('2020'), max: new Date('2030') }) fc.option(fc.integer()) // T | undefined // Object fc.record({ id: fc.uuid(), email: fc.emailAddress(), age: fc.integer({ min: 0, max: 120 }), }) // Tuple fc.tuple(fc.string(), fc.integer()) // One of fc.oneof(fc.integer(), fc.string()) ``` ### Stateful test (model-based) ```ts import fc, { Command } from 'fast-check'; class Stack { private arr: T[] = []; push(x: T) { this.arr.push(x); } pop(): T | undefined { return this.arr.pop(); } size() { return this.arr.length; } } class PushCommand implements Command<{ size: number }, Stack> { constructor(readonly value: number) {} check = (m: { size: number }) => true; run(m: { size: number }, r: Stack) { r.push(this.value); m.size++; expect(r.size()).toBe(m.size); } } class PopCommand implements Command<{ size: number }, Stack> { check = (m: { size: number }) => m.size > 0; run(m, r) { r.pop(); m.size--; expect(r.size()).toBe(m.size); } } test('stack', () => { fc.assert(fc.property( fc.commands([ fc.integer().map(v => new PushCommand(v)), fc.constant(new PopCommand()), ], { maxCommands: 100 }), (cmds) => fc.modelRun(() => ({ model: { size: 0 }, real: new Stack() }), cmds), )); }); ``` ### Real-world: JSON serialize ```ts test('JSON parse(stringify(x)) = x', () => { fc.assert( fc.property(fc.jsonValue(), (x) => { expect(JSON.parse(JSON.stringify(x))).toEqual(x); }) ); }); ``` ### 검증 ```ts import { z } from 'zod'; const User = z.object({ email: z.string().email(), age: z.number().int().positive() }); test('valid input always parses', () => { fc.assert(fc.property( fc.record({ email: fc.emailAddress(), age: fc.integer({ min: 1, max: 200 }), }), (input) => { expect(() => User.parse(input)).not.toThrow(); }, )); }); ``` ### Sort idempotent ```ts test('sorted is idempotent', () => { fc.assert(fc.property( fc.array(fc.integer()), (arr) => { const s1 = [...arr].sort(); const s2 = [...s1].sort(); expect(s2).toEqual(s1); }, )); }); ``` ### Shrinking 효과 ```ts // 실패 시 input 자동 최소화 // 처음: [42, -123, 0, 9999, ...] (10 items, 실패) // shrink 후: [0, -1] (2 items, 같은 실패) // → 디버깅 쉬움 ``` ### Reproduce 실패 ```ts fc.assert(fc.property(...), { seed: 1234, path: '5:0:1' }); // 같은 seed = 같은 input ``` ### 통계 출력 ```ts fc.statistics( fc.integer(), (n) => n < 0 ? 'negative' : n === 0 ? 'zero' : 'positive', 100, ); // 분포 확인 — 충분히 다양? ``` ## 🤔 의사결정 기준 | 적합 | 부적합 | |---|---| | 순수 함수 | 외부 부수효과 | | Encode/decode round-trip | UI 상호작용 | | Sort / search algorithm | API integration | | Parser / formatter | Auth / DB | | 수학 / 변환 | E2E flow | ## ❌ 안티패턴 - **Property = 1 case**: 일반 unit test 와 같음. universal 속성 찾기. - **Generator 너무 좁음**: bug 못 찾음. - **너무 넓음 (모든 string)**: shrinking 비싸짐. 적절히. - **외부 부수효과 (DB)**: random 에 안 맞음. mock. - **Random 결과를 assertion 으로**: 결정론적 property. - **Run 횟수 너무 적음 (10)**: edge case 놓침. 100+. - **`fc.assert` 없이 single random**: 의미 없음. ## 🤖 LLM 활용 힌트 - Round-trip / 단조성 / idempotency / 길이 보존 같은 일반 속성 찾기. - Encoder / parser / sort / search 가 강 candidate. - shrinking 이 디버깅 무기. ## 🔗 관련 문서 - [[Testing_Test_Pyramid]] - [[Testing_Faker_and_Builders]] - [[TS_Schema_Validation_Comparison]]