Learn Go← Dashboard

Built-in testing: go test, table tests, benchmarks, fuzzing.

Writing Tests with testing.T

Tests in Go are first-class — no external framework required. Put a _test.go file next to the code under test, write functions starting with Test, and run go test.

A first test

// math.go
package math

func Add(a, b int) int { return a + b }
// math_test.go
package math

import "testing"

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

Run with:

go test ./...
go playground
Loading...

Test API basics

| Method | Use | | ------------- | --------------------------------------------------------------- | | t.Error(...) | Report a failure and continue (still runs the rest of the test). | | t.Errorf(...) | Same with Sprintf formatting. | | t.Fatal(...) | Report failure and stop this test. | | t.Skip(...) | Skip the test (with a reason). | | t.Helper() | Mark this function as a helper — failures show the caller's line. |

Table-driven tests

The idiomatic style for many similar cases: a slice of structs, looped over.

go playground
Loading...

Real code uses t.Run(tc.name, func(t *testing.T) { ... }) so each row shows up as its own test. The playground above just simulates it.

Useful flags

go test ./...               # everything
go test -run TestAdd        # filter by name (regex)
go test -v                  # verbose, prints each test
go test -race ./...         # run race detector — catches data races
go test -cover ./...        # coverage summary
go test -count=1            # disable result caching (always re-run)

The race detector (-race) is invaluable once you start writing goroutines.

Subtests with t.Run

t.Run gives each row of a table test its own name in the output, lets you filter them (go test -run TestFoo/zero), and isolates t.Fatal to that subtest:

for _, tc := range tests {
    t.Run(tc.name, func(t *testing.T) {
        if got := Add(tc.a, tc.b); got != tc.want {
            t.Errorf("Add(%d,%d) = %d; want %d", tc.a, tc.b, got, tc.want)
        }
    })
}

Setup, teardown, and t.Cleanup

For per-test cleanup that runs even when the test fails or panics, prefer t.Cleanup over defer. It composes nicely across helpers:

func newTempFile(t *testing.T) string {
    t.Helper()
    f, _ := os.CreateTemp("", "test-*")
    t.Cleanup(func() { os.Remove(f.Name()) })
    return f.Name()
}

Test files & _test packages

  • foo_test.go in the same package gives you access to unexported names — "white-box" testing.
  • A file declared package foo_test instead can only see the exported API — "black-box". The two can coexist.
`t.Error` vs `t.Fatal` — what's the difference?