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.

In plain English Plain English: a table-driven test turns many similar scenarios into data, so adding one more edge case is a row, not a copy-pasted test function.

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
    })
}
testify: 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.

internal/spike/modules/email/application/commands/create_email_handler_test.go:22
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 })
}
Anchor Repo-side you'll see the full table form in repository tests (notification/.../repositories/media_test.go, tagmgmt/repositories/tag_test.go) and the t.Run-per-scenario form in handler tests (create_email_handler_test.go). Match whichever the file you're editing already uses.
Backend use case Backend use case: validators, command handlers, mappers, and status-code translation logic often have many small scenarios that fit neatly into named case tables.
Common mistake Common mistake: hiding too much logic inside the loop and making each row hard to understand. Senior tests keep the case struct simple and the assertion story obvious.
Read this next

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.

quii.gitbook.io/learn-go-with-tests
pkg.go.dev/testing

Check yourself (from memory)

Q1. A table-driven test iterates over…

Cases as struct rows, each run as a named subtest. Adding a scenario is one line.

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.
Sketch the shape of an idiomatic table-driven test.
recall, then click to reveal
Define 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.
Want the golden-file, fuzzing, or t.Cleanup patterns too? Ask me.

1. testing package; Learn Go with Tests.