Fuzz Testing in Go
Write Go fuzz tests with testing.F to find crashes and edge cases automatically, run them with -fuzz, and turn failures into permanent regression tests.
Table tests check the inputs you thought of. Fuzzing checks inputs you did not think of. Go's fuzzer, built into go test since Go 1.18, generates random variations of seed inputs and runs your code on them, looking for panics and failed checks.
A Function With a Hidden Bug
// file: reverse_test.go
package reverse
import (
"testing"
"unicode/utf8"
)
// Reverse reverses a string by runes.
func Reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < j; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}
func FuzzReverse(f *testing.F) {
for _, seed := range []string{"hello", "", "Go docs", "नमस्ते"} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, s string) {
once := Reverse(s)
twice := Reverse(once)
if utf8.ValidString(s) && s != twice {
t.Errorf("Reverse(Reverse(%q)) = %q", s, twice)
}
if utf8.ValidString(s) && !utf8.ValidString(once) {
t.Errorf("Reverse(%q) produced invalid UTF-8 %q", s, once)
}
})
}
- A fuzz test is
FuzzXxx(f *testing.F). f.Addgives seed inputs, normal examples the fuzzer starts from.f.Fuzztakes a function whose first parameter is*testing.T, followed by the input types.
Inside, you cannot compare against a fixed expected value, because inputs are random. Instead you check properties that must always hold: reversing twice gives the original, valid text stays valid.
Running It
Plain go test runs only the seed inputs, like a normal test:
go test -run FuzzReverse
To actually fuzz, pass -fuzz and usually a time limit:
go test -fuzz=FuzzReverse -fuzztime=30s
The fuzzer prints progress such as fuzz: elapsed: 3s, execs: 180000 (60000/sec), new interesting: 12. If it finds a failure, it stops and saves the input.
Invalid UTF-8 inputs:
The fuzzer generates any bytes, including strings that are not valid UTF-8. Converting those to []rune replaces bad bytes, so Reverse(Reverse(s)) would not equal s. That is why the checks above only apply when utf8.ValidString(s) is true. Deciding what inputs your function should handle is part of writing a good fuzz test.
When the Fuzzer Finds Something
A failure looks like this:
--- FAIL: FuzzReverse (0.25s)
--- FAIL: FuzzReverse (0.00s)
reverse_test.go:22: Reverse("\xe4") produced invalid UTF-8 ...
Failing input written to testdata/fuzz/FuzzReverse/a1b2c3d4e5f6
To re-run:
go test -run=FuzzReverse/a1b2c3d4e5f6
The failing input is saved under testdata/fuzz/FuzzReverse/. From then on, plain go test runs it every time, so the bug can never come back silently. Commit that folder with your fix.
What to Fuzz
Fuzzing pays off most for code that handles untrusted or complex input:
- parsers for formats like JSON, CSV, URLs or custom protocols
- encoders and decoders, where
Decode(Encode(x)) == xis a natural property - input validation and sanitizing functions
- anything that must never panic, whatever it receives
Supported input types are strings, []byte, integers, floats, bool and rune.
Fuzz targets must be deterministic and fast:
The function given to f.Fuzz runs millions of times. It should not use the network, sleep, or depend on global state that changes between calls, or failures will not reproduce.
Fuzzing in CI:
-fuzz runs until it finds a failure or hits -fuzztime, and only one fuzz target per package at a time. In CI, usually run plain go test (which replays the seeds and saved failures), and run real fuzzing separately with a time limit.
A useful fuzz test:
It has realistic seeds, checks properties that must always hold, skips input your function legitimately rejects, and any failure it finds ends up as a file in testdata/fuzz alongside the fix.
Summary
Fuzz tests are FuzzXxx(f *testing.F) functions with seeds from f.Add and a property check in f.Fuzz. Plain go test runs the seeds, and go test -fuzz=Name -fuzztime=30s generates new inputs. Failing inputs are saved under testdata/fuzz and replayed forever after. Fuzz parsers, encoders and validators, and keep fuzz targets fast and deterministic.