Frontend Labs
⋯

19. Testing in Go: table-driven tests and subtests

19.1 The convention

Test files end in _test.go, sit beside the code they test, and test functions take *testing.T:

// storage_test.go
package storage

import "testing"

func TestSet(t *testing.T) {
	s := New()
	s.Set("key", "value")
	got, ok := s.Get("key")
	if !ok || got != "value" {
		t.Fatalf("got (%q, %v), want (%q, true)", got, ok, "value")
	}
}

19.2 Table-driven tests

The idiomatic way to cover many cases without duplicating test logic:

func TestParseSize(t *testing.T) {
	tests := []struct {
		name    string
		input   string
		want    int64
		wantErr bool
	}{
		{"plain bytes", "100", 100, false},
		{"kilobytes", "10KB", 10 * 1024, false},
		{"empty input", "", 0, true},
		{"garbage unit", "10XB", 0, true},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := ParseSize(tt.input)
			if (err != nil) != tt.wantErr {
				t.Fatalf("err = %v, wantErr %v", err, tt.wantErr)
			}
			if got != tt.want {
				t.Errorf("got %d, want %d", got, tt.want)
			}
		})
	}
}

t.Run gives each case its own named subtest — failures report exactly which case broke (TestParseSize/garbage_unit), and -run TestParseSize/kilo re-runs just that one.

19.3 Test helpers, t.Helper(), and fixtures

func newTestStore(t *testing.T) *Store {
	t.Helper() // failures blame the caller's line, not this one
	s := New()
	t.Cleanup(func() { s.Close() }) // runs after the test, even on failure
	return s
}

19.4 Real-world example

Day 10's table-driven tests for the storage interface (empty key, oversized value, concurrent reads, deleting a missing key) is exactly the shape of a real PR's test suite — one table, one assertion block, four edge cases documented as data instead of four near-duplicate test functions.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36