Skip to content

Day 048 — Fuzzing (testing.F) & Examples

Month 2 · Week 3 · ⬅ Day 047 · Day 049 ➡ · Journal index

🎯 Learning Objective

Write a native Go fuzz test with testing.F that asserts invariants over generated inputs, and write runnable, output-verified Example functions that double as documentation.

📚 Topics

  • func FuzzXxx(f *testing.F) · f.Add seed corpus · f.Fuzz(func(t, ...))
  • Property/invariant thinking (round-trips, idempotence) · the corpus on disk
  • func ExampleXxx() with // Output: comments

📖 Reading / Sources

📝 Notes

  • A fuzz test seeds with f.Add(...) and runs f.Fuzz(func(t *testing.T, in T){...}). Fuzzable arg types are limited: []byte, string, numeric types, bool, rune. The seed args and the f.Fuzz callback params must match in type and order. → [[fuzzing]]
  • go test (no flags) runs the fuzz target against only the seed corpus — fast, deterministic, part of CI. go test -fuzz=FuzzXxx enters the mutation engine, generating new inputs until a failure or you stop it (-fuzztime=30s to bound it).
  • Fuzzing checks properties/invariants, not exact outputs, because you don't know the answer for a random input. Classics: round-trip (Decode(Encode(x)) == x), idempotence (f(f(x)) == f(x)), "never panics", "output stays valid UTF-8".
  • When the engine finds a failing input it writes it to testdata/fuzz/FuzzXxx/<hash>. That file becomes a permanent regression seed — commit it.
  • Example functions live in _test.go, are compiled, and if they end with a // Output: comment, go test runs them and checks stdout matches. No // Output: → compiled but not run. → [[examples-as-tests]]
  • Naming wires examples into godoc: ExampleReverse documents Reverse, ExampleReverse_unicode is a named variant. // Unordered output: matches lines in any order (good for map ranges).
  • Examples are the rare test that's also rendered documentation on pkg.go.dev — keep them clean and realistic.

💻 Code Examples

func FuzzReverse(f *testing.F) {
    for _, s := range []string{"", "a", "Hello", "héllo", "go🚀"} {
        f.Add(s) // seed corpus
    }
    f.Fuzz(func(t *testing.T, s string) {
        got := Reverse(s)
        if Reverse(got) != s { // invariant 1: round trip
            t.Errorf("Reverse(Reverse(%q)) != %q", s, s)
        }
        if utf8.ValidString(s) && !utf8.ValidString(got) { // invariant 2
            t.Errorf("Reverse(%q) produced invalid UTF-8", s)
        }
    })
}

func ExampleReverse() {
    fmt.Println(Reverse("Hello"))
    // Output: olleH
}

Runnable property-check demo (shows fuzzing's mindset & a UTF-8 bug): examples/month-02/fuzz-roundtrip/ · Run: go run ./examples/month-02/fuzz-roundtrip Real FuzzReverse: exercises/month-02/week-3/reverse/ · Fuzz: go test -fuzz=FuzzReverse -fuzztime=10s ./exercises/month-02/week-3/reverse

🏋️ Exercises / Practice

Exercise Status Link
FuzzReverse round-trip + UTF-8 invariants exercises/month-02/week-3/reverse
Watch a byte-reverse fail the invariant examples/month-02/fuzz-roundtrip

🐛 Mistakes Made

  • Mismatched seed types: f.Add(0) (int) with f.Fuzz(func(t, s string)) — fuzzing panics if the corpus types don't match the callback. Made them line up.
  • Asserted an exact output in a fuzz body — impossible for random input; switched to an invariant.

❓ Open Questions

  • How does the engine minimize a failing input to the smallest reproducer? (Shrinking — it trims/mutates toward a minimal case before saving to testdata.)

🧠 Active Recall (answer without looking)

  1. Q: What does plain go test do with a FuzzXxx target?
    A

Runs it only against the seed corpus (f.Add values + any saved testdata cases) — deterministic, no new inputs. Use -fuzz=FuzzXxx to actually mutate/generate. 2. Q: Why must fuzz tests assert properties, not exact outputs?

A

The inputs are generated/unknown, so you can't precompute the answer. You assert invariants (round-trip, idempotence, never-panics, stays-valid) that must hold for any input.

🪶 Feynman Reflection

Fuzzing is a robot that keeps inventing weird inputs trying to break a rule you stated must always hold — like "reversing twice gives me back the original." When it finds a breaker, it saves the exact input forever so the bug can't sneak back. Example functions are tests whose printed output is checked and shown as documentation.

🕳️ Knowledge Gaps

  • Fuzzing structured inputs (structs, multiple args) and coverage-guided corpus growth internals.

✅ Summary

I can write a property-based fuzz test that runs seeds in CI and mutates under -fuzz, and write Example functions that verify output and document the API.

⏭️ Next Steps / Prep for Tomorrow

  • Day 049: week review + closed-book recall of the whole testing toolkit.

Time spent Difficulty Confidence
90 min 🟦🟦🟦⬜⬜ 🟦🟦🟦⬜⬜

Suggested commit: test(week-3): testing.F fuzzing and example functions (day 048)