Overview
Example-based tests only check the inputs someone thought of. Two related techniques
generate inputs instead: fuzzing throws coverage-guided semi-random data at code to find
crashes and panics, while property-based testing (PBT) checks stated invariants over
structured random inputs and shrinks failures to minimal counterexamples. Go ships one of
them natively — fuzzing has been in the toolchain since 1.18 — and the community's best
PBT library is pgregory.net/rapid (flyingmutant/rapid), with gopter as the older,
heavier option.
Key points
- Native fuzzing:
func FuzzXxx(f *testing.F), seed withf.Add(...), thenf.Fuzz(func(t *testing.T, data []byte, n int) { ... }). Runs as a normal seed-driven test undergo test; actual fuzzing needsgo test -fuzz=FuzzXxx. - Coverage-guided: the engine mutates inputs and keeps those that reach new coverage — the same idea as libFuzzer/AFL, integrated with the build cache.
- Corpus management: interesting inputs accumulate in the build cache
(
$GOCACHE/fuzz); crashing inputs are written totestdata/fuzz/FuzzXxx/and — this is the payoff — run as regression seeds in every plaingo testforever after. Commit them. - Fuzz-arg types are limited to primitives,
stringand[]byte; decode richer structures from[]byteinside the fuzz target. - OSS-Fuzz integration: Google's OSS-Fuzz runs native Go fuzz targets continuously for accepted open-source projects — free compute for parser-heavy libraries.
- rapid for PBT:
rapid.Check(t, func(t *rapid.T) { ... })with combinator generators (rapid.Int(),rapid.SliceOf,rapid.Custom,.Filter,.Map) drawn viaDraw(t, "name"); failures shrink automatically to a minimal case, no reflection circus. gopter predates it — more machinery, less ergonomic. - State-machine testing: rapid's
t.Repeatdrives random sequences of operations against your implementation and a simple model — the technique that finds lifecycle bugs (put/get/delete interleavings) no table test will. - Complementary, not competing: keep the table test for known cases, add a property
for the invariant, add a fuzz target at trust boundaries — all three run under
go test.
Details
When each fits
| Technique | Sweet spot | Typical properties |
|---|---|---|
| Fuzzing | Parsers, decoders, protocol handlers, anything consuming untrusted bytes | "never panics", "never reads OOB", differential vs reference impl |
| PBT (rapid) | Business invariants, data structures, codecs | round-trip (decode(encode(x)) == x), idempotence, commutativity, model conformance |
Fuzzing optimises for reaching weird states (coverage feedback, byte-level mutation); PBT optimises for expressing invariants over well-typed inputs and explaining failures (shrinking). A round-trip check is often worth writing both ways.
Working habits
- Seed fuzz corpora from real-world samples and past bug reports — the mutator starts far ahead.
- Keep fuzz targets deterministic and side-effect-free; sub-second per input, or the engine starves.
- In CI, run
go test(seeds + committed crashers) always; schedule bounded fuzzing (-fuzz -fuzztime=2mper target) nightly rather than per-PR. - Name
Drawcalls in rapid — they label the shrunken counterexample output.
Examples
func FuzzParseAddr(f *testing.F) {
f.Add("127.0.0.1:8080")
f.Add("[::1]:80")
f.Fuzz(func(t *testing.T, s string) {
addr, err := ParseAddr(s)
if err != nil {
return // invalid input is fine; panics are not
}
// Round-trip property doubles as the oracle.
if got, err := ParseAddr(addr.String()); err != nil || got != addr {
t.Fatalf("round-trip failed: %q -> %v -> %v (%v)", s, addr, got, err)
}
})
}
func TestQueueModel(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
q := NewQueue[int]()
var model []int
t.Repeat(map[string]func(*rapid.T){
"push": func(t *rapid.T) {
v := rapid.Int().Draw(t, "v")
q.Push(v)
model = append(model, v)
},
"pop": func(t *rapid.T) {
if len(model) == 0 {
return
}
if got := q.Pop(); got != model[0] {
t.Fatalf("pop = %d, want %d", got, model[0])
}
model = model[1:]
},
})
})
}
Related
- Testing in Go — strategies & tooling map — parent map of the strategy portfolio.
- Unit testing — the table tests these techniques extend.
- Security testing — fuzzing worn as a security hat at trust boundaries.
- Property-based testing in Java — jqwik's take on the same ideas, minus native fuzzing.