Lesson 20 · Testing, tooling & performance
Table-driven tests
The Go testing idiom — one row per case, in our repo's exact style.
Your win: write an idiomatic table-driven test with subtests and testify, matching the shape reviewers expect in this codebase.
Testing is built into the language
No framework needed: a file ending _test.go, a function
func TestXxx(t *testing.T), and go test ./.... Subtests come
from t.Run(name, func(t *testing.T){...}), and t.Parallel()
lets independent tests run concurrently.1
The table-driven pattern
Go's signature testing idiom: describe cases as a slice of structs, then loop, running each as a named subtest. One row per scenario — adding a case is one line.
cases := []struct {
name string
in Input
want Output
wantErr bool
}{
{name: "happy path", in: ..., want: ...},
{name: "empty input", in: ..., wantErr: true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := Fn(tc.in) // act
if tc.wantErr { require.Error(t, err); return }
require.NoError(t, err)
assert.Equal(t, tc.want, got) // assert
})
}
require vs assert
require.X stops the test on failure (use when continuing
is pointless — e.g. a nil result you're about to deref). assert.X
records the failure and continues (use to collect multiple field
checks). Both read like English: require.NoError, assert.Equal,
assert.ErrorIs.
Our repo's exact shape
The documented conventions (.claude/rules/go-test-style.md):
t.Parallel(), a context.WithTimeout, testify, the
// arrange // act // assert comments, and t.Run subtests.
func TestCreateEmailHandler(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
...
t.Run("happy case", func(t *testing.T) { // arrange / act / assert inside })
}
t.Run-per-scenario form in handler tests
(create_email_handler_test.go). Match whichever the file
you're editing already uses.
Learn Go with Tests + the testing package
The best hands-on path (TDD from lesson one), plus the authoritative API. Mocks — the other half of our tests — are Lesson 21.
Check yourself (from memory)
Q1. A table-driven test iterates over…
Q2. testify's require differs from assert in that require…
require halts (fatal); assert
records and continues. Use require before a deref, assert to collect field checks.
Q3. In our repo, tests begin with t.Parallel() and…
t.Parallel() + a bounded
context.WithTimeout, then testify mocks — the documented house style.
cases := []struct{ name string; in ...; want ...;
wantErr bool }{...}; then for _, tc := range cases { t.Run(tc.name,
func(t *testing.T){ t.Parallel(); got, err := Fn(tc.in); if tc.wantErr {
require.Error(t, err); return }; require.NoError(t, err); assert.Equal(t, tc.want,
got) }) }. One row per case, subtests name each, arrange/act/assert inside. Our
repo adds context.WithTimeout and testify mocks.t.Cleanup patterns too? Ask
me.