전통 test = "이 입력에 이 출력". 그런데 100 example 도 안 잡는 edge case 가 production 에서. Property-based testing 은 generator 가 수천 input 자동 생성 + 입력 → 입력의 invariant (속성) 만 검증. 이 가이드는 generator / shrinker / QuickCheck 계열의 메커니즘과, "이것만 잡는 버그" 의 type 을 정리한다.
Example-based vs Property-based
Example-based (전통):
test("reverse twice = original", () => {
expect(reverse(reverse([1,2,3]))).toEqual([1,2,3]);
expect(reverse(reverse([]))).toEqual([]);
expect(reverse(reverse([7]))).toEqual([7]);
// ... 5 example 추가
});
→ 5 example 만 검증. [1, NaN, undefined, [], "x"] 같은 edge case?
Property-based:
test("reverse twice = original", () => {
fc.assert(
fc.property(fc.array(fc.anything()), (arr) => {
expect(reverse(reverse(arr))).toEqual(arr);
})
);
});
→ fc.array(fc.anything()) generator 가 1000+ array 자동 생성.
string, number, null, undefined, NaN, nested array, ... 모두 시도.
속성 깨지는 첫 case 발견 시 fail.Property — 무엇을 검증하나
"입력 → 출력" 의 universal 한 관계.
흔한 property 유형:
1. Roundtrip (역연산):
parse(serialize(x)) === x
decode(encode(x)) === x
2. Idempotence (반복 적용):
normalize(normalize(x)) === normalize(x)
sort(sort(x)) === sort(x)
3. Invariant (불변):
sort(arr).length === arr.length
max(arr) >= min(arr)
4. Equivalence (다른 구현 비교):
slowButCorrect(x) === fastNewImpl(x)
nativeRegex(x) === customRegex(x)
5. Algebraic laws:
(a + b) + c === a + (b + c) // associativity
merge(a, b) === merge(b, a) // commutativityGenerator — 입력 자동 생성
fast-check (TypeScript) 예:
fc.integer() → -2147483648..2147483647
fc.integer({min: 0, max: 100}) → 0..100
fc.string() → 임의 문자열 (empty 포함)
fc.string({minLength: 5}) → 5+ 글자
fc.array(fc.integer()) → 임의 길이의 int array
fc.record({ → 객체
id: fc.integer(),
name: fc.string(),
active: fc.boolean()
})
fc.oneof(fc.integer(), fc.string()) → int 또는 string
→ "어떤 input 도 가능" 의 의도를 명시.
edge case (empty array, max int, "" string, NaN, ...) 자동 포함.Shrinker — 가장 작은 실패 input
fail 발견 시 generator 가 같은 fail 일으키는 더 작은 input 시도.
처음 fail: arr = [42, 17, 88, -3, 1024] → reverse(reverse) 결과 깨짐
shrink 1: arr = [42, 17, 88, -3] → 여전히 fail?
shrink 2: arr = [42, 17, 88] → 여전히 fail?
shrink 3: arr = [42, 17] → fail!
shrink 4: arr = [42] → pass (1 element 는 OK)
shrink 5: arr = [17] → pass
shrink 6: arr = [0, 0] → fail!
shrink 7: arr = [] → pass
→ minimal failing input: [0, 0]
"두 element 가 같으면 깨진다" 같은 패턴 발견 쉬움
→ debug 의 가치 큼. random [42, 17, 88, -3, 1024] 보다 [0, 0] 이
훨씬 원인 파악 빠름.이 방식만 잡는 버그 type
1. Edge case (empty, 0, max, NaN)
function discount(items) {
return items.reduce((sum, i) => sum + i.price, 0) * 0.9;
}
example-based: [{price:100}, {price:200}] → 270 ✓
property-based 발견:
items = [] → result = 0 * 0.9 = 0 (예상 동작?)
items = [{price: NaN}] → result = NaN ← bug!
items = [{price: Infinity}] → result = Infinity ← edge2. Roundtrip 실패
function escapeQuotes(s) { return s.replace(/'/g, "\\'"); }
function unescapeQuotes(s) { return s.replace(/\\'/g, "'"); }
property: unescape(escape(s)) === s
발견: s = "\\'" → escape: "\\\\'" → unescape: "\\'" ✗
(backslash 이미 있는 경우 처리 안 됨)3. Concurrent / state machine 버그
fast-check / Hypothesis 의 state machine testing:
임의 method 호출 sequence 생성 → invariant 검증.
예: cache.set(k, v) + cache.get(k) + cache.evict(k) 임의 순서
property: get(k) === set 한 마지막 v (또는 evict 후 undefined)
→ 손으로는 시도 안 했을 sequence (set k1, evict k2, get k1) 자동 시도.라이브러리
| 언어 | 라이브러리 | 비고 |
|---|---|---|
| Haskell (원조) | QuickCheck (1999) | property-based 의 시작 |
| Python | Hypothesis | 가장 mature, stateful 강함 |
| JavaScript / TypeScript | fast-check | 가장 active, Jest/Vitest 통합 |
| Rust | proptest, quickcheck | proptest 가 modern 표준 |
| Java / Kotlin | jqwik, Kotest property | |
| Scala | ScalaCheck | Haskell QuickCheck port |
| Go | testing/quick (stdlib), gopter | stdlib 의 quick 은 기본만 |
흔한 함정
- 모든 test 를 property 로 — 명확한 example 이 더 읽기 쉬운 경우 많음 (regression test 등). 보완 관계.
- property 가 너무 약함 — `result !== null` 같은 trivial property. roundtrip / invariant / equivalence 가 가치 있는 property.
- generator 가 너무 제약 — 의도한 edge case 못 만남. fc.anything() 또는 wide range 시도.
- test 시간 폭증 — default 100 run × test 50 개 = CI 느려짐. 중요 path 만 property, 나머지 example.
- seed 안 박음 — 다른 사람이 같은 fail 재현 못함. fail 시 seed 출력 + 그 seed 박는 test 추가 (regression).
마무리
Property-based testing 의 본질 — "수많은 example 을 손으로 만드는 대신 input 의 universal property 만 명시". generator + shrinker 가 edge case 자동 발견 + minimal repro 자동 생성.
실용 — 모든 test 를 property 로 안 함. parser / serializer / algorithm 의 핵심 함수에 property test 추가, 나머지는 example-based. QuickCheck / Hypothesis / fast-check 의 stateful machine 검증도 활용 가치 큼.