Fuzz Testing — Built-in Fuzzing in Go 1.18+
Fuzz testing has been built into Go's standard library since Go 1.18. It automatically generates random, unexpected inputs to find bugs, panics, infinite loops, and memory errors.
What is Fuzz Testing?
Fuzz testing is a technique that automatically generates random and unexpected inputs to find crashes, panics, infinite loops, memory errors, and other bugs.
When is it useful?
- Parsers and serialization/deserialization code
- Cryptographic functions
- Code that handles user input
- Complex algorithms with boundary conditions
Writing Basic Fuzz Tests
// reverse.go
package main
import "unicode/utf8"
// Reverse a UTF-8 string
func Reverse(s string) string {
b := []byte(s)
for i, j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1 {
b[i], b[j] = b[j], b[i]
}
return string(b)
}
// Correct implementation (rune-aware)
func ReverseRune(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
// reverse_test.go
package main
import (
"testing"
"unicode/utf8"
)
// Regular unit test (also serves as seed corpus)
func TestReverse(t *testing.T) {
tests := []struct {
input string
want string
}{
{"", ""},
{"abc", "cba"},
{"Hello, 世界", "界世 ,olleH"}, // This case fails with Reverse!
}
for _, tt := range tests {
if got := ReverseRune(tt.input); got != tt.want {
t.Errorf("ReverseRune(%q) = %q, want %q", tt.input, got, tt.want)
}
}
}
// Fuzz test — uses testing.F
func FuzzReverse(f *testing.F) {
// Seed corpus: example inputs
f.Add("Hello, World")
f.Add("")
f.Add("a")
f.Add("abc")
// Fuzz function: Go automatically generates mutated inputs
f.Fuzz(func(t *testing.T, s string) {
// Property-based test: double reverse equals original
reversed := ReverseRune(s)
doubleReversed := ReverseRune(reversed)
if s != doubleReversed {
t.Errorf("double reverse failed: original=%q, double_reversed=%q", s, doubleReversed)
}
// Verify UTF-8 validity
if !utf8.ValidString(reversed) {
t.Errorf("reversed result is not valid UTF-8: %q", reversed)
}
})
}
Running Fuzz Tests
# Run with seed corpus only (like normal tests, fast)
go test -run=FuzzReverse
# Run in fuzz mode (generates infinite new inputs)
go test -fuzz=FuzzReverse
# Fuzz for 30 seconds
go test -fuzz=FuzzReverse -fuzztime=30s
# Specify parallel workers
go test -fuzz=FuzzReverse -parallel=4
Corpus File Management
When fuzzing finds a bug, it automatically saves it to the corpus file.
testdata/
└── fuzz/
└── FuzzReverse/
├── 48f3a8c9b2e1d5f7 ← input that found a bug
└── a1b2c3d4e5f6a7b8
# Reproduce the found bug
go test -run=FuzzReverse/48f3a8c9b2e1d5f7
# View corpus file content
cat testdata/fuzz/FuzzReverse/48f3a8c9b2e1d5f7
Corpus files should be committed to Git. Running go test in CI uses the seed corpus as regression tests.
Real-World Fuzz Testing Examples
JSON Parser Fuzzing
func FuzzJSONParse(f *testing.F) {
// Seeds: valid JSON examples
f.Add(`{"name":"Kim Golang","age":30}`)
f.Add(`[]`)
f.Add(`null`)
f.Add(`"hello"`)
f.Fuzz(func(t *testing.T, data string) {
// Should not panic
var v interface{}
err := json.Unmarshal([]byte(data), &v)
if err == nil {
// If parsing succeeded, re-serialization should also succeed
out, err2 := json.Marshal(v)
if err2 != nil {
t.Errorf("parsing succeeded but marshaling failed: %v", err2)
}
// Re-parsing should also succeed
var v2 interface{}
if err3 := json.Unmarshal(out, &v2); err3 != nil {
t.Errorf("re-parsing failed: %v", err3)
}
}
// err != nil case is normal — parser correctly rejected invalid JSON
})
}
URL Parser Fuzzing
func FuzzURLParse(f *testing.F) {
f.Add("https://example.com/path?query=1#fragment")
f.Add("http://localhost:8080")
f.Add("")
f.Add("/relative/path")
f.Fuzz(func(t *testing.T, rawURL string) {
u, err := url.Parse(rawURL)
if err != nil {
return // parsing failure is normal
}
// Invariant: reconstructed URL should parse to same components
reconstructed := u.String()
u2, err := url.Parse(reconstructed)
if err != nil {
t.Errorf("reconstructed URL parsing failed: %q → %q, err: %v",
rawURL, reconstructed, err)
return
}
if u.Host != u2.Host || u.Path != u2.Path {
t.Errorf("URL round-trip mismatch: original=%v, reconstructed=%v", u, u2)
}
})
}
Custom Parser Fuzzing
// Fuzz a custom config parser
func FuzzConfigParse(f *testing.F) {
f.Add("key=value\n")
f.Add("# comment\nkey=value\n")
f.Add("")
f.Fuzz(func(t *testing.T, input string) {
// Should not panic
cfg, err := ParseConfig(input)
if err != nil {
return // parsing error is acceptable
}
// Property: serialized config should re-parse to equivalent
serialized := cfg.String()
cfg2, err := ParseConfig(serialized)
if err != nil {
t.Errorf("serialized config parsing failed: input=%q, serialized=%q",
input, serialized)
return
}
if !cfg.Equal(cfg2) {
t.Errorf("round-trip mismatch: %v != %v", cfg, cfg2)
}
})
}
Fuzzing Multiple Input Types
// Fuzzing multiple parameters
func FuzzAdd(f *testing.F) {
f.Add(int64(0), int64(0))
f.Add(int64(1), int64(-1))
f.Add(math.MaxInt64, int64(1))
f.Fuzz(func(t *testing.T, a, b int64) {
// Addition should not overflow without checking
result := safeAdd(a, b)
// Commutative property
if safeAdd(b, a) != result {
t.Errorf("commutative property violated: safeAdd(%d, %d) != safeAdd(%d, %d)",
a, b, b, a)
}
})
}
// Supported types:
// string, []byte, bool, byte, rune,
// float32, float64, int, int8, int16, int32, int64,
// uint, uint8, uint16, uint32, uint64
Integrating Fuzz Tests with Unit Tests
func FuzzHTTPHandler(f *testing.F) {
// Valid seed requests
f.Add("GET", "/users/1", "")
f.Add("POST", "/users", `{"name":"test"}`)
f.Add("DELETE", "/users/999", "")
f.Fuzz(func(t *testing.T, method, path, body string) {
req := httptest.NewRequest(method, path, strings.NewReader(body))
w := httptest.NewRecorder()
// Should not panic
handler(w, req)
resp := w.Result()
// Must return valid HTTP status code
if resp.StatusCode < 100 || resp.StatusCode > 599 {
t.Errorf("invalid HTTP status code: %d", resp.StatusCode)
}
})
}
CI Integration Strategy
# .github/workflows/fuzz.yml
name: Fuzz Tests
on:
schedule:
- cron: '0 2 * * *' # daily at 2 AM
jobs:
fuzz:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: '1.22'
# Run like unit tests with seed corpus (fast, for PRs)
- name: Run fuzz tests (seed corpus)
run: go test -run=FuzzXxx ./...
# Long-running fuzzing (nightly schedule)
- name: Run fuzzing (5 minutes)
run: go test -fuzz=FuzzXxx -fuzztime=5m ./...
Key Summary
| Item | Details |
|---|---|
| Function prefix | FuzzXxx(f *testing.F) |
| Add seeds | f.Add(value1, value2, ...) |
| Fuzz function | f.Fuzz(func(t *testing.T, args...)) |
| Run unit tests | go test -run=FuzzXxx |
| Run fuzzing | go test -fuzz=FuzzXxx |
| Corpus path | testdata/fuzz/FuzzXxx/ |
Property-Based Testing Principles:
- Double reverse (round-trip) invariant:
reverse(reverse(x)) == x - Serialization/deserialization round-trip:
parse(serialize(x)) == x - Commutative property:
f(a, b) == f(b, a) - Boundary conditions: empty input, max values, special characters