Irbisa · Cheatsheet

Testing & Benchmarks

Table tests, httptest, benchmarks, fuzzing, -race in CI

Middle Developer · 25 questions

  1. 01

    How does go test find and run tests, and what do ./..., -run, -v and -count=1 do?

    go test compiles the package together with its _test.go files into a test binary and runs every func TestXxx(t *testing.T) in it; files and functions that do not follow those names are ignored.

  2. 02

    What is a table-driven test in Go, and what does wrapping each case in t.Run add over a plain for loop?

    A table-driven test lists its cases as a slice of structs (name, input, expected result) and runs one assertion over each; t.Run turns every case into a subtest with its own name and its own *testing.T.

    func TestParseDuration(t *testing.T) {	cases := []struct {		name    string		in      string		want    time.Duration		wantErr bool…
  3. 03

    What is the difference between t.Error and t.Fatal, and when do you use each?

    t.Error marks the test as failed and lets it continue; t.Fatal marks it failed and stops it immediately, so nothing after that line runs.

  4. 04

    Why does a test helper start with t.Helper(), and why is its first parameter usually testing.TB rather than *testing.T?

    t.Helper() makes a failure inside the helper point at the line that called it, not at the helper's own t.Fatalf; testing.TB is the interface that *testing.T, *testing.B and *testing.F all implement, so one helper serves tests, benchmarks and fuzz targets.

    // 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 {…
  5. 05

    Why do some test files declare package foo_test instead of package foo, and what do you give up?

    package foo_test is an external test package: it sits in the same directory and ends up in the same test binary, but compiles separately, so it sees only foo's exported API and may import packages that themselves import foo.

    // 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…
  6. 06

    What is an Example function with an // Output: comment, and how does go test check it?

    An ExampleXxx() function in a _test.go file is documentation that runs: go test executes it, captures its standard output and compares it with the // Output: comment, and pkg.go.dev shows it next to Xxx.

    package money_test import (	"fmt" 	"example.com/money"…
  7. 07

    What can t.Cleanup do that a defer inside the same helper cannot, and when is defer still the better tool?

    t.Cleanup(f) ties teardown to the test, not to the current function, so a helper can release what it opened instead of handing the caller a func() to remember.

    // 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() }}…
  8. 08

    What is TestMain, and when do you need it instead of per-test setup?

    func TestMain(m *testing.M) replaces the generated main of a package's test binary: it runs once per package, does the setup, calls m.Run() to run every test, then tears down.

    var testDB *sql.DB func TestMain(m *testing.M) {	flag.Parse() // needed before testing.Short() or custom flags	if testing.Short() {		m.Run()…
  9. 09

    This test hangs until the 10-minute -timeout whenever fetch fails. 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()}

    t.Fatalf stops only the goroutine that calls it (here the worker, not the test) because it exits through runtime.Goexit.

  10. 10

    With go 1.21 in go.mod, every parallel subtest below checks the last case; after bumping the line to go 1.22 it 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)		}	})}

    Before Go 1.22 a range loop had one tc variable, reassigned on every iteration.

  11. 11

    Find the bug: this table test stayed green after a change that made ParsePort reject 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)			}		})	}}

    The subtest returns on any error without looking at tc.wantErr, so an unexpected error makes a valid case pass.

  12. 12

    When do you test with httptest.NewRecorder and when with httptest.NewServer, and what does the recorder not reproduce?

    The recorder tests a handler, the test server tests a client: httptest.NewRecorder is a fake http.ResponseWriter you pass straight to ServeHTTP, while httptest.NewServer starts a real HTTP server on a loopback port.

    // 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()…
  13. 13

    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?

    Make the service depend on small interfaces declared where they are used, and pass a test double in the test: a hand-written fake in most cases, a generated mock (gomock, mockery) only when the exact calls are the behaviour under test.

    // 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}…
  14. 14

    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?

    Inject the clock: the code gets "now" from a dependency (a func() time.Time field or a one-method interface), production passes time.Now, and the test passes a fake it moves forward by hand.

    type Session struct {	expires time.Time	now     func() time.Time // the seam} func NewSession(ttl time.Duration, now func() time.Time) *Session {…
  15. 15

    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?

    Keep the expected output in testdata/ and rewrite it only under your own -update flag: a normal run compares against a committed file, and regenerating it shows up as a diff in code review.

    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)))…
  16. 16

    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 ./...?

    Unit-test your logic with fakes; write integration tests for what your code only assumes about the other side: SQL, migrations, transactions, wire formats.

    //go:build integration package store_test // imports: database/sql, os, testing, example.com/store, pgx stdlib…
  17. 17

    What does go test -cover measure, and why does 100% coverage not mean the code is well tested?

    Coverage measures which statements the tests executed, not whether anything was checked.

  18. 18

    How do you write a benchmark in Go, how is b.N chosen, and what does b.Loop change?

    A benchmark is func BenchmarkXxx(b *testing.B) in a _test.go file, run with go test -bench=Xxx; the framework repeats the body until the run lasts -benchtime (1s by default) and reports the time per operation.

    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() {…
  19. 19

    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?

    A fuzz target states a property instead of an example: f.Fuzz runs your function on coverage-guided mutations of the seed inputs, so it reaches inputs nobody would think to write down.

    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")…
  20. 20

    How do you make a test fail when the code under test leaks goroutines?

    Check which goroutines are still alive when the test ends: go.uber.org/goleak does it with defer goleak.VerifyNone(t) in one test or goleak.VerifyTestMain(m) for the whole package, and fails with the stacks of the goroutines left behind.

    // 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) }()…
  21. 21

    This benchmark reports 0.25 ns/op with b.N at 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)	}}

    An empty loop.

  22. 22

    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)	}}

    slices.Sort sorts data in place, so only the first iteration sees shuffled input; every later one sorts an already sorted slice, which pdqsort finishes in near-linear time.

  23. 23

    One go test -bench run before your change and one after show a 9% speed-up. Why does that prove nothing, and what does a sound comparison look like?

    One run per side is one sample.

  24. 24

    How do you test code with timeouts, tickers and background goroutines without real waiting or an injected clock, and what does testing/synctest do?

    synctest.Test (Go 1.25) runs the test function in a "bubble" with a fake clock: time inside moves only when every goroutine in the bubble is blocked, so a one-minute timeout passes instantly and deterministically, and synctest.Wait() waits until background goroutines settle.

    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")…
  25. 25

    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?

    Retries hide the bug; make the suite hostile instead.