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