Skip to content

Writing Tests with go test

Go’s test runner discovers tests through two rules enforced by the toolchain:

  1. File name ends in _test.go — the compiler excludes these files from normal builds.
  2. Function name starts with Test followed by a capital letter — TestAdd, TestParseURL, etc.

A minimal test file for a math.go source file lives in the same package and directory:

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

Run it with:

Terminal window
go test ./... # run all tests in all packages
go test -v ./... # verbose: print each test name and PASS/FAIL
go test -run TestAdd # run only tests whose name matches the regex

Both record a failure, but they differ in whether the test continues:

FunctionMarks failureStops test?
t.ErrorfYesNo — test continues
t.FatalfYesYes — test stops immediately

Use t.Fatalf when a later assertion would panic if the earlier one failed (e.g., dereferencing a pointer that you just checked is nil).

func TestDivide(t *testing.T) {
result, err := Divide(10, 0)
if err == nil {
t.Fatalf("expected an error for division by zero, got nil")
}
_ = result
}

The idiomatic Go pattern for testing many inputs against the same function is a table-driven test: a slice of struct cases iterated with t.Run. Each sub-test gets its own name, failure message, and — with -v — its own line in the output.

fibonacci_test.go
package main
import "testing"
func TestFibonacci(t *testing.T) {
cases := []struct {
name string
n int
want int
}{
{"fib(0)", 0, 0},
{"fib(1)", 1, 1},
{"fib(5)", 5, 5},
{"fib(10)", 10, 55},
}
for _, tc := range cases {
tc := tc // capture range variable (pre-Go 1.22)
t.Run(tc.name, func(t *testing.T) {
got := Fibonacci(tc.n)
if got != tc.want {
t.Errorf("Fibonacci(%d) = %d; want %d", tc.n, got, tc.want)
}
})
}
}

Run with -v to see each sub-test:

Terminal window
go test -v -run TestFibonacci ./...
# --- PASS: TestFibonacci (0.00s)
# --- PASS: TestFibonacci/fib(0) (0.00s)
# --- PASS: TestFibonacci/fib(1) (0.00s)
# --- PASS: TestFibonacci/fib(5) (0.00s)
# --- PASS: TestFibonacci/fib(10) (0.00s)

Add -race to catch data races at test time — this is mandatory in CI:

Terminal window
go test -race -coverprofile=coverage.out -covermode=atomic ./...

The -covermode=atomic flag is required alongside -race for accurate coverage counts because the race detector rewrites memory accesses.

What file suffix does the Go toolchain require for test files?
What is the difference between t.Errorf and t.Fatalf?
In a table-driven test, what does t.Run provide for each case?
Why is -covermode=atomic required when using -race?