Files
2nd/10_Wiki/Topics/Coding/Testing_Property_Based.md
T
2026-05-09 21:08:02 +09:00

5.3 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
testing-property-based Property-based Testing — fast-check / 자동 input Coding draft B conceptual 2026-05-09 2026-05-09
testing
property-based
fast-check
vibe-coding
language applicable_to
TS / fast-check
Backend
Frontend
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 기본

import fc from 'fast-check';
import { describe, test, expect } from 'vitest';

function reverse<T>(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

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)

import fc, { Command } from 'fast-check';

class Stack<T> {
  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<number>> {
  constructor(readonly value: number) {}
  check = (m: { size: number }) => true;
  run(m: { size: number }, r: Stack<number>) {
    r.push(this.value);
    m.size++;
    expect(r.size()).toBe(m.size);
  }
}

class PopCommand implements Command<{ size: number }, Stack<number>> {
  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

test('JSON parse(stringify(x)) = x', () => {
  fc.assert(
    fc.property(fc.jsonValue(), (x) => {
      expect(JSON.parse(JSON.stringify(x))).toEqual(x);
    })
  );
});

검증

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

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 효과

// 실패 시 input 자동 최소화
// 처음: [42, -123, 0, 9999, ...] (10 items, 실패)
// shrink 후: [0, -1] (2 items, 같은 실패)
// → 디버깅 쉬움

Reproduce 실패

fc.assert(fc.property(...), { seed: 1234, path: '5:0:1' });
// 같은 seed = 같은 input

통계 출력

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 이 디버깅 무기.

🔗 관련 문서