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

218 lines
5.3 KiB
Markdown

---
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<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
```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<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
```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]]