Notes — Module 11: Testing and Benchmarking¶
These are your personal study notes. Write freely and honestly. Incomplete notes are fine — they show where your understanding still needs work. Return to this file to add insights as they develop over time.
Module: [[go/11. Testing and Benchmarking]] Topic: [[go]] Date started: YYYY-MM-DD Status: In progress
Concept Map¶
Sketch how the concepts in this module relate to each other. Fill in the Mermaid diagram.
mindmap
root((Testing & Benchmarking))
testing package
_test.go convention
package foo vs package foo_test
TestXxx signature
go test flags
-run regex
-v verbose
-race data race detector
-cover / -coverprofile
-bench / -benchmem
-fuzz / -fuzztime
-count bypass cache
Test functions
t.Error vs t.Fatal
t.Errorf / t.Fatalf
t.Skip / t.Cleanup
t.Helper for helpers
Table-driven tests
struct slice of cases
t.Run subtests
t.Parallel
loop variable capture bug
TestMain
package-level setup/teardown
m.Run
os.Exit(code)
Fakes vs mocks
interface injection
hand-written fakes
why not heavy mock frameworks
httptest
httptest.NewRecorder
httptest.NewRequest
httptest.NewServer
Benchmarks
BenchmarkXxx signature
b.N — framework-controlled
b.ResetTimer
b.ReportAllocs
ns/op allocs/op reading output
benchstat for comparisons
Fuzzing
FuzzXxx signature
f.Add seed corpus
f.Fuzz invariant function
testdata/fuzz/ crashers
Examples
ExampleXxx with Output comment
runnable documentation
Coverage
-coverprofile
go tool cover -html
what coverage does vs does not guarantee
Alternative: draw this on paper, photo it, and link the image here.
Key Insights¶
The "aha moments" — the things that, once understood, made the rest clear. Be specific: "I finally understood X because Y" is more useful than "X makes sense".
- t.Helper() and failure location: I finally understood that without
t.Helper(), every failure from a helper function points to the helper's source line — useless when the helper is called from 10 different tests.t.Helper()redirects the failure message to the caller. This is one of those details that seems minor until you spend 20 minutes debugging which test failed. - b.N is not yours to set: The benchmark framework runs the loop until results are stable — you never choose N. Your only job is to put exactly one unit of work inside the loop. Setting N yourself would give meaningless results.
- Add insights as you discover them
My Understanding¶
Explain the core concepts in your own words, as if teaching them to someone else. If you can't explain it simply, you don't understand it well enough yet.
t.Error vs t.Fatal¶
Your explanation here
What I'm still unsure about: (e.g., what happens if t.Fatal is called in a goroutine spawned by the test, not from the test itself)
Table-driven tests¶
Your explanation here
What I'm still unsure about: (e.g., when should I use a named struct type vs an anonymous struct for the test cases table)
Benchmarks and b.N¶
Your explanation here
What I'm still unsure about: (e.g., what does the -8 suffix mean in benchmark output like BenchmarkXxx-8)
Fuzzing¶
Your explanation here
What I'm still unsure about: (e.g., how does the fuzzer know which mutations are "interesting" vs redundant)
Interfaces and fakes¶
Your explanation here
What I'm still unsure about: (e.g., at what point does a fake become complex enough that you should use a library instead)
Connections to Other Topics¶
How does this module connect to things you already know?
| This module's concept | Connects to | How |
|---|---|---|
| Interface-based fakes | [[go/6. Methods and Interfaces]] | Fakes are just types that implement the required interface; Go's structural typing means you can define a narrow test-only interface that the real type satisfies without any changes to production code |
| Checking errors in tests | [[go/8. Error Handling]] | errors.Is and errors.As are how you verify that a function returned a specific error type, not just any error; wrapping errors with %w enables this in tests |
| Coverage and profiling | [[go/16. Performance and Profiling]] | Coverage profiles and pprof profiles use the same -coverprofile / -cpuprofile flags; profiling during benchmarks is covered in Module 16 |
Questions That Arose¶
Log questions as they appear. Don't stop to answer them now — just capture them. Then move the serious ones to QUESTIONS.md.
- Does
t.Parallel()inside at.Runsubtest make the subtest parallel with siblings, or parallel with top-level tests? → added to QUESTIONS.md as Q001 - What happens when a fuzz test crashes — does the crasher file contain the exact input, or a mutation of it? → might be answered in go.dev/security/fuzz
- Is there a way to run only the fuzz seed corpus without running the full test suite? →
go test -run FuzzXxxshould do it
Code Snippets Worth Remembering¶
Patterns, idioms, or examples that captured something important.
The canonical table-driven test skeleton¶
func TestXxx(t *testing.T) {
tests := []struct {
name string
input TypeA
want TypeB
wantErr bool
}{
{"case name", input, want, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Xxx(tt.input)
if (err != nil) != tt.wantErr {
t.Fatalf("error = %v; wantErr %v", err, tt.wantErr)
}
if got != tt.want {
t.Errorf("got %v; want %v", got, tt.want)
}
})
}
}
Why I'm saving this: This skeleton covers 80% of all tests I'll ever write. The wantErr bool pattern for error checking, t.Fatalf on error mismatch, and t.Errorf on value mismatch — all in one place.
The t.Helper pattern for test helpers¶
func assertNoError(t *testing.T, err error) {
t.Helper() // must be first line
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
}
Why I'm saving this: t.Helper() as the first line of every helper function. Easy to forget, high cost when forgotten.
Benchmark skeleton with reset¶
func BenchmarkXxx(b *testing.B) {
setup := doExpensiveSetup()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = functionUnderTest(setup)
}
}
Why I'm saving this: The b.ResetTimer() after setup is the thing I'll forget most often.
Fuzz test skeleton¶
func FuzzXxx(f *testing.F) {
f.Add("seed1") // add representative seed inputs
f.Add("seed2")
f.Fuzz(func(t *testing.T, input string) {
result, err := Xxx(input)
if err != nil {
return // invalid input — the function correctly rejected it
}
// assert invariant about result
_ = result
})
}
Why I'm saving this: The key point: return early if err != nil, then assert the invariant on successful results.
What Tripped Me Up¶
Mistakes I made, misconceptions I had, things that confused me more than they should have. Being honest here helps you later.
- t.Fatal in a loop — I initially used
t.Fatalfinside a range over test cases withoutt.Run, which stopped at the first failure. Switching tot.Run+t.Errorfgives you all failures at once. - Benchmark timer — I initially started the measurement before setup was complete. The
b.ResetTimer()call must come after all setup, not before it.
Summary in My Own Words¶
Write a 3–5 sentence summary of this entire module without looking at any notes. If you can't do this, you need more study time.
Write your summary here after completing the module.
Last updated: YYYY-MM-DD