Quick Syntax
func FuzzParseID(f *testing.F) {
f.Add("user-123")
f.Add("")
f.Fuzz(func(t *testing.T, raw string) {
id, err := ParseID(raw)
if err == nil && id.String() == "" {
t.Fatalf("empty id from %q", raw)
}
})
}go test ./...
go test -run=^$ -fuzz=FuzzParseID -fuzztime=30sf.Add의 seed와 fuzz target은 인수의 개수·타입·순서가 같아야 합니다. fuzz target은 여러 worker에서 병렬로 실행되므로 호출 사이에 상태를 남기지 않습니다.
fuzz 흐름
fuzz test는 seed corpus와 fuzz target으로 구성된다
Go fuzz test는 *_test.go 안의 func FuzzXxx(f *testing.F) 형태로 작성합니다. 하나의 fuzz test에는 f.Fuzz(...) target을 하나만 두고, f.Add(...)와 testdata/fuzz/<FuzzName>/의 모든 seed는 target의 fuzz 인수와 타입·순서가 정확히 같아야 합니다.
fuzz 인수에는 string, []byte, 정수형, float32/float64, bool만 쓸 수 있습니다. f.Add("user-123")를 등록했다면 target도 func(t *testing.T, raw string)이어야 합니다. 일반 go test에서는 seed corpus가 회귀 테스트처럼 실행되고, -fuzz flag를 주면 engine이 입력을 변형하면서 새 실패를 찾습니다.
fuzz target은 빠르고 결정적이어야 한다
fuzzing은 같은 target을 매우 많이 반복 실행합니다. target은 여러 worker에서 병렬·비결정 순서로 호출될 수 있으므로 전역 상태, 현재 시간, 네트워크, 파일 시스템 상태에 의존하거나 호출 사이 상태를 남기면 안 됩니다. parser, encoder/decoder, validator, 문자열 처리, binary format 처리처럼 순수 함수에 가까운 경계가 fuzzing에 잘 맞습니다.
f.Fuzz(func(t *testing.T, input []byte) {
out, err := Decode(input)
if err != nil {
t.Skip()
}
if !Valid(out) {
t.Fatalf("invalid decode: %#v", out)
}
})입력이 "관심 없는 invalid input"이면 실패가 아니라 t.Skip()으로 제외할 수 있습니다. 다만 Decode가 error를 반환하는 것이 정상이라면 성공한 결과의 불변식만 검사하는 편이 더 명확합니다. panic, t.Fatal, t.Error, 무한 loop나 deadlock으로 target 실행 시간이 한도를 넘는 경우는 실패 입력으로 기록됩니다.
실패 입력은 회귀 테스트 자산이 된다
fuzzing 중 실패가 나오면 Go는 최소화한 실패 입력을 testdata/fuzz/<FuzzName>/... 아래에 저장하고, 재실행 명령을 출력합니다. 버그를 고친 뒤 일반 go test만 실행해도 seed corpus가 다시 실행되므로, 같은 입력이 회귀 테스트가 됩니다. $GOCACHE/fuzz의 생성 corpus와 달리 testdata/fuzz의 실패 입력은 버전에 포함해 관리합니다.
어디에 쓸까
| 상황 | 적합한 선택 |
|---|---|
| parser나 decoder가 예외 입력에 약할 때 | fuzz test |
| table test로 케이스를 다 쓰기 어려울 때 | seed + fuzz target |
| 실패 입력을 회귀 테스트로 남길 때 | testdata/fuzz 관리 |
| target이 느리거나 외부 I/O가 필요할 때 | fuzzing보다 unit/integration test |
| CI에서 짧게 돌릴 때 | -fuzztime 지정 |
주의할 점
fuzzing은 기본적으로 실패가 나거나 사용자가 멈출 때까지 실행될 수 있습니다. 로컬 탐색, 짧은 CI 실행, 장시간 보안 탐색을 같은 명령으로 운영하지 말고 -fuzztime, 대상 package, -parallel worker 수를 명확히 나누는 편이 안전합니다. coverage-guided 탐색은 현재 지원되는 계측 플랫폼에서 의미 있게 확장됩니다.
go test -run=^$ -fuzz=FuzzParseID -fuzztime=30s장시간 실행할수록 더 많은 입력을 탐색할 수 있지만, 빠른 검증 루프에서는 시간 상한을 두는 편이 좋습니다.
참고 링크
2 sources