Fuzz Tests
Finding edge cases with randomized inputs
What Fuzzing Does
Fuzz testing feeds random inputs to your code and watches for crashes, panics, or assertion failures. It finds bugs that humans don't think to test for: malformed UTF-8, integer overflows, nil pointer dereferences on unusual inputs, and other edge cases hiding in code paths that manual tests never exercise.
Go has built-in fuzzing since Go 1.18. You write a fuzz test that looks similar to a regular test, provide some seed inputs, and the fuzzer mutates those seeds to explore the input space. When it finds an input that causes a failure, it saves that input so the bug becomes a regression test.
When to Write Fuzz Tests
Fuzz tests shine for code that processes untrusted input. Parsing functions, encoding and decoding logic, input validation, and cryptographic operations are all good candidates. If your function could receive arbitrary bytes from the network or user input, fuzzing will probably find bugs in it.
They're less useful for business logic with complex preconditions. A function that requires a valid database connection and authenticated user is hard to fuzz meaningfully because most random inputs won't satisfy those preconditions.
Writing Your First Fuzz Test
A fuzz test starts with Fuzz instead of Test and takes *testing.F instead of *testing.T. You add seed inputs with f.Add(), then define the fuzz function that receives mutated versions of those seeds.
The seed corpus matters. Good seeds help the fuzzer find interesting code paths faster. Include normal inputs, edge cases, and inputs that have caused bugs before.
Skipping Invalid Inputs
Not all inputs make sense for every test. If your function requires a specific key length, skip inputs that don't meet that requirement rather than letting them fail:
Use t.Skip() liberally. The fuzzer will generate lots of invalid inputs, and skipping them lets it focus on the interesting ones.
Testing Security Properties
Fuzz tests are excellent for security-sensitive code. You can verify that tampering with data is always detected:
This test verifies that any single-byte modification to the ciphertext causes decryption to fail. A passing test gives confidence in the authentication properties of the encryption.
Running Fuzz Tests
During normal test runs, fuzz tests execute only with their seed corpus:
To actually fuzz (generate new inputs), use go test directly with the -fuzz flag:
When fuzzing finds a failure, Go saves the failing input to testdata/fuzz/<TestName>/. This input becomes part of the seed corpus, so the bug is automatically tested in future runs.
What to Do When Fuzzing Finds a Bug
When the fuzzer reports a failure, you'll see the input that caused it. First, write a regular unit test that reproduces the bug with that specific input. This ensures the bug stays fixed and documents the edge case.
Then fix the bug. The fuzz-discovered input in testdata/fuzz/ will continue running as part of the seed corpus, giving you ongoing regression protection.
Bazel Configuration
Fuzz tests are included in regular test targets:
The testdata/** glob picks up the fuzz corpus so discovered inputs are tested in CI. Bazel runs fuzz tests with their seed corpus only; actual fuzzing happens locally with go test -fuzz.