Skip to main content

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

ItemDetails
Function prefixFuzzXxx(f *testing.F)
Add seedsf.Add(value1, value2, ...)
Fuzz functionf.Fuzz(func(t *testing.T, args...))
Run unit testsgo test -run=FuzzXxx
Run fuzzinggo test -fuzz=FuzzXxx
Corpus pathtestdata/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