Irbisa · Cheatsheet
Testing & Benchmarks
Table tests, httptest, benchmarks, fuzzing, -race in CI
How does
go testfind and run tests, and what do./...,-run,-vand-count=1do?What is a table-driven test in Go, and what does wrapping each case in
t.Runadd over a plainforloop?func TestParseDuration(t *testing.T) { cases := []struct { name string in string want time.Duration wantErr bool…What is the difference between
t.Errorandt.Fatal, and when do you use each?Why does a test helper start with
t.Helper(), and why is its first parameter usuallytesting.TBrather than*testing.T?// testing.TB, not *testing.T: the same fixture works in a benchmark.func mustStore(tb testing.TB, seed ...Row) *Store { tb.Helper() // failures below are reported at the CALLER's line s, err := Open(tb.TempDir()) if err != nil {…Why do some test files declare
package foo_testinstead ofpackage foo, and what do you give up?// store/export_test.go: package store, compiled only under `go test`package store // Give the external tests a controlled view of the internals.var ParseKey = parseKey…What is an
Examplefunction with an// Output:comment, and how doesgo testcheck it?package money_test import ( "fmt" "example.com/money"…What can
t.Cleanupdo that adeferinside the same helper cannot, and when isdeferstill the better tool?// wrong: the caller has to remember the teardown, and some will notfunc openDBWithCleanup(t *testing.T) (*sql.DB, func()) { db := dial(t) return db, func() { db.Close() }}…What is
TestMain, and when do you need it instead of per-test setup?var testDB *sql.DB func TestMain(m *testing.M) { flag.Parse() // needed before testing.Short() or custom flags if testing.Short() { m.Run()…This test hangs until the 10-minute
-timeoutwheneverfetchfails. Why, and how should the goroutine report the error?func TestFetch(t *testing.T) { var wg sync.WaitGroup wg.Add(1) go func() { resp, err := fetch(context.Background(), "/health") if err != nil { t.Fatalf("fetch: %v", err) } check(t, resp) wg.Done() }() wg.Wait()}With
go 1.21ingo.mod, every parallel subtest below checks the last case; after bumping the line togo 1.22it works. Why?for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { t.Parallel() if err := Validate(tc.in); err == nil { t.Errorf("Validate(%+v) = nil, want error", tc.in) } })}Find the bug: this table test stayed green after a change that made
ParsePortreject every port.func TestParsePort(t *testing.T) { cases := []struct { in string want int wantErr bool }{{"8080", 8080, false}, {"70000", 0, true}, {"http", 0, true}} for _, tc := range cases { t.Run(tc.in, func(t *testing.T) { got, err := ParsePort(tc.in) if err != nil { return // the error cases } if got != tc.want { t.Errorf("ParsePort(%q) = %d, want %d", tc.in, got, tc.want) } }) }}When do you test with
httptest.NewRecorderand when withhttptest.NewServer, and what does the recorder not reproduce?// Handler under test: no socket, no client, no transport.func TestCreateOrder(t *testing.T) { req := httptest.NewRequest(http.MethodPost, "/orders", strings.NewReader(`{"sku":"A1","qty":2}`)) rec := httptest.NewRecorder()…How do you unit-test a service that calls a database and an external API without running either, and do you hand-write fakes or generate mocks?
// Declared in the service package: only what the service calls.type UserRepo interface { User(ctx context.Context, id int64) (User, error) Save(ctx context.Context, u User) error}…A token-expiry test sleeps two seconds and still fails about once a week on CI. How do you test expiry without depending on the real clock?
type Session struct { expires time.Time now func() time.Time // the seam} func NewSession(ttl time.Duration, now func() time.Time) *Session {…A renderer test embeds a 200-line expected string that breaks on every template tweak. How do you move to golden files without a test that can never fail?
var update = flag.Bool("update", false, "rewrite testdata golden files") func TestRenderInvoice(t *testing.T) { for _, name := range []string{"single_line", "multi_currency", "zero_total"} { t.Run(name, func(t *testing.T) { got := normalise(Render(loadCase(t, name)))…When is a unit test enough and when do you need an integration test, and how do you keep tests that need a real Postgres out of the default
go test ./...?//go:build integration package store_test // imports: database/sql, os, testing, example.com/store, pgx stdlib…What does
go test -covermeasure, and why does 100% coverage not mean the code is well tested?How do you write a benchmark in Go, how is
b.Nchosen, and what doesb.Loopchange?func BenchmarkEncode(b *testing.B) { for _, n := range []int{10, 1000} { b.Run(fmt.Sprintf("rows=%d", n), func(b *testing.B) { rows := makeRows(n) // setup: outside b.Loop, not timed b.ReportAllocs() for b.Loop() {…A header parser passes 40 table cases yet panicked in production on a real client's input. How does a fuzz target differ, and when is one worth adding?
func FuzzParseHeader(f *testing.F) { // Seeds are the cases you already know. Without -fuzz they run as a // plain unit test, so CI keeps them forever for free. f.Add("Bearer abc123") f.Add("") f.Add("Bearer \x00\x00")…How do you make a test fail when the code under test leaks goroutines?
// Leaks: the channel is unbuffered, so once First has returned,// the slower senders block on `ch <-` forever.func First(ctx context.Context, urls []string) string { ch := make(chan string) for _, u := range urls { go func() { ch <- fetch(ctx, u) }()…This benchmark reports
0.25 ns/opwithb.Nat 1000000000. What did it actually measure, and how do you fix it?func scale(v, f int) int { return v*f + 1 } func BenchmarkScale(b *testing.B) { for i := 0; i < b.N; i++ { scale(i, 3) }}Find the bug: this benchmark sorts 100,000 random ints in 0.1 ms, while the same sort takes about 10 ms in production.
func BenchmarkSort(b *testing.B) { data := rand.Perm(100_000) // shuffled 0..99999 for b.Loop() { slices.Sort(data) }}One
go test -benchrun before your change and one after show a 9% speed-up. Why does that prove nothing, and what does a sound comparison look like?How do you test code with timeouts, tickers and background goroutines without real waiting or an injected clock, and what does
testing/synctestdo?func TestCacheEvictsAfterTTL(t *testing.T) { synctest.Test(t, func(t *testing.T) { c := NewCache(time.Minute) // starts a janitor goroutine with a Ticker defer c.Close() // the janitor must exit before Test returns c.Set("k", "v")…About one CI run in twenty fails: never the same test, always green on a re-run. The team has started adding retries. How do you turn that into a bug someone can fix?