CHAPTER 31 · QUALITY & SHIPPING

Testing

Testing in Go is built into the language and its tooling. No separate frameworks, no annotations, no configuration. Write a file ending in _test.go, name your functions TestXxx, run go test. That's it.

Learning objectives

  • Write and run unit tests with the testing package.
  • Structure tests using the table-driven pattern.
  • Use t.Run for subtests.
  • Mark helpers with t.Helper() for better stack traces.
  • Measure coverage and write HTTP handler tests with httptest.

Your first test

math.go:

package mymath

func Add(a, b int) int { return a + b }

math_test.go:

package mymath

import "testing"

func TestAdd(t *testing.T) {
    got := Add(2, 3)
    if got != 5 {
        t.Errorf("Add(2, 3) = %d, want 5", got)
    }
}

Rules:

  • File ends in _test.go.
  • Function is named Test + CapitalName.
  • Takes a single *testing.T.
  • Use t.Errorf to fail without stopping, t.Fatalf to fail and stop.

Running tests

go test .                  # run this package
go test ./...              # run every package in the module
go test -v ./...           # verbose: show each test
go test -run TestAdd .     # run only tests matching regex
go test -race ./...        # with race detector (Chapter 24)
go test -timeout 30s ./... # set a timeout

Table-driven tests

Most idiomatic pattern in Go: a slice of test cases + a loop:

func TestAdd(t *testing.T) {
    cases := []struct {
        name       string
        a, b, want int
    }{
        {"positive", 2, 3, 5},
        {"negative", -2, -3, -5},
        {"zero",     0, 0, 0},
        {"mix",      -1, 5, 4},
    }
    for _, c := range cases {
        t.Run(c.name, func(t *testing.T) {
            got := Add(c.a, c.b)
            if got != c.want {
                t.Errorf("Add(%d, %d) = %d, want %d", c.a, c.b, got, c.want)
            }
        })
    }
}

Add a case → run it. No new test function, no copy-paste.

Subtests with t.Run

t.Run creates a named child test. Benefits:

  • Clearer output (TestAdd/positive, TestAdd/negative).
  • You can target a single case: go test -run TestAdd/negative.
  • Failed subtests don't stop others.

Test helpers with t.Helper()

func assertEqual(t *testing.T, got, want any) {
    t.Helper()                  // report errors at the CALL SITE, not inside this func
    if !reflect.DeepEqual(got, want) {
        t.Errorf("got %v, want %v", got, want)
    }
}

Without t.Helper(), error messages point to the assertion function. With it, they point to your actual test code. Always call t.Helper() first in test helper functions.

t.Cleanup

func TestWithDB(t *testing.T) {
    db := openTestDB(t)
    t.Cleanup(func() { db.Close() })    // runs when THIS test (or subtest) exits

    // ... use db ...
}

t.Cleanup is like defer but test-aware. It runs after the test, in LIFO order, even on failure.

Example functions (doc + test in one)

func ExampleAdd() {
    fmt.Println(mymath.Add(2, 3))
    // Output: 5
}

The compiler runs the example, captures stdout, compares it to the // Output: comment. The example also appears in the package's generated documentation. Two birds, one function.

Coverage

go test -cover ./...                              # summary
go test -coverprofile=cov.out ./...; go tool cover -html=cov.out

The HTML report highlights which lines were executed: green for covered, red for not. Use it as a diagnostic, not a score (100% coverage with bad tests is worse than 60% with good ones).

HTTP handler tests with net/http/httptest

func TestHello(t *testing.T) {
    req := httptest.NewRequest("GET", "/hello?name=Alice", nil)
    rec := httptest.NewRecorder()

    helloHandler(rec, req)

    if rec.Code != 200 {
        t.Errorf("status = %d, want 200", rec.Code)
    }
    if !strings.Contains(rec.Body.String(), "Alice") {
        t.Errorf("body = %q, want to contain 'Alice'", rec.Body.String())
    }
}

No real HTTP server required. ResponseRecorder captures the output so you can assert on it. For integration-style tests, httptest.NewServer spins up a real listener on a random port.

Check your understanding

Practice exercises

EXERCISE 1

Table-test a reverse function

Write Reverse(s string) string (from Chapter 7) and a table-driven test covering empty strings, single ASCII, multibyte ("héllo"), and emoji. Ensure the test fails if reverse is broken.

Show solution sketch
func TestReverse(t *testing.T) {
    cases := []struct{ name, in, want string }{
        {"empty", "", ""},
        {"ascii", "abc", "cba"},
        {"multibyte", "héllo", "olléh"},
        {"emoji", "ab🌍", "🌍ba"},
    }
    for _, c := range cases {
        t.Run(c.name, func(t *testing.T) {
            if got := Reverse(c.in); got != c.want {
                t.Errorf("Reverse(%q) = %q, want %q", c.in, got, c.want)
            }
        })
    }
}

Further reading

Tests in the bank.