{} The Go Reference

Testing · Stdlib · Beginner

Testing

The testing package and go test — writing TestXxx functions, Errorf vs Fatalf, t.Helper, and the got/want convention that runs with go test ./...

Testing Beginner ⏱ 8 min read Complete

🧪 Analogy

A test is a lab assistant that checks your work the same way every time. You hand it a known input, tell it the answer you want, and it shouts only when what it got disagrees. go test rounds up every assistant in the package, runs them, and reports who complained — no manual main to babysit.

Your first test

A test lives next to the code it checks, in a file ending _test.go, and is any function shaped func TestXxx(t *testing.T). Here’s a tiny function and its test:

// math.go
package math

func Abs(x int) int {
	if x < 0 {
		return -x
	}
	return x
}
// math_test.go
package math

import "testing"

func TestAbs(t *testing.T) {
	got := Abs(-3)
	want := 3
	if got != want {
		t.Errorf("Abs(-3) = %d, want %d", got, want)
	}
}

The got / want naming is a strong Go convention — it makes failure messages read like a sentence: got 5, want 3. You can run the real logic here in main to see it execute:

abs.go — editable & runnable
package main

import "fmt"

// Abs is the function under test.
func Abs(x int) int {
if x < 0 {
	return -x
}
return x
}

func main() {
for _, x := range []int{-3, 0, 7} {
	fmt.Printf("Abs(%d) = %d\n", x, Abs(x))
}
}

Errorf vs Fatalf, and t.Helper

t.Errorf marks the test failed but keeps running — good for independent checks so you see them all at once. t.Fatalf marks it failed and stops the test function — use it when continuing would crash or is pointless (a nil you’d dereference, an err that means nothing else is valid):

func TestParse(t *testing.T) {
	v, err := Parse("12")
	if err != nil {
		t.Fatalf("Parse returned error: %v", err) // stop: v is unusable
	}
	if v != 12 {
		t.Errorf("Parse = %d, want 12", v)        // keep going
	}
}

When you extract a repeated assertion into a helper, call t.Helper() first so failures point at the caller’s line, not at the helper’s internals:

func assertEqual(t *testing.T, got, want int) {
	t.Helper() // failures are reported at the line that called assertEqual
	if got != want {
		t.Errorf("got %d, want %d", got, want)
	}
}
graph TD
A["go test ./..."] --> B["compile *_test.go with package"]
B --> C["find every TestXxx(t *testing.T)"]
C --> D["run each test"]
D --> E{"t.Errorf called?"}
E -->|"no"| F["PASS"]
E -->|"yes / t.Fatalf"| G["FAIL: print got vs want"]

Running tests

go test builds and runs the tests in the current package. The flags you’ll reach for daily:

# run tests in this package
$ go test
ok      example/math    0.002s

# run tests in every package under the module
$ go test ./...

# -v prints each test name and PASS/FAIL
$ go test -v
=== RUN   TestAbs
--- PASS: TestAbs (0.00s)
PASS
ok      example/math    0.003s

# -run filters by regexp on the test name
$ go test -run TestAbs -v

A failing run shows the file, line, and your message:

$ go test
--- FAIL: TestAbs (0.00s)
    math_test.go:8: Abs(-3) = -3, want 3
FAIL
exit status 1
FAIL    example/math    0.002s

🐹 Test the behaviour, not the implementation

Write assertions against what a function should return or do, not how it does it internally. Then you can refactor the body freely and the test still guards the contract. And prefer many small focused tests over one giant test — when several t.Errorf checks are independent, you learn about all the breakages in a single run instead of fixing them one at a time.

Setup, cleanup, and temp dirs

Tests that touch files, databases, or servers need setup and teardown. Two testing features make this clean and leak-free. t.Cleanup(fn) registers teardown that runs when the test (and its subtests) finish — in LIFO order, even after a t.Fatal. Unlike a defer in the test body, it can be registered inside a helper, so a helper owns both ends of a resource. t.TempDir() hands you a fresh directory that’s deleted automatically:

func TestWritesFile(t *testing.T) {
	dir := t.TempDir() // unique; auto-removed when the test ends
	path := filepath.Join(dir, "out.txt")

	if err := Save(path, "data"); err != nil {
		t.Fatalf("Save: %v", err)
	}
	got, _ := os.ReadFile(path)
	if string(got) != "data" {
		t.Errorf("file = %q, want %q", got, "data")
	}
}

// A helper that owns its own teardown via t.Cleanup.
func newTestServer(t *testing.T) *Server {
	t.Helper()
	s := startServer()
	t.Cleanup(s.Stop) // runs automatically at test end
	return s
}

Related controls: t.Skip/t.Skipf bail out of a test that can’t run here (e.g. if testing.Short() { t.Skip(...) } under go test -short); t.Setenv sets an env var for the duration of the test; and a package-level TestMain(m *testing.M) wraps one-time setup/teardown around the whole package’s run (os.Exit(m.Run())).

The t toolbox

MethodEffect
t.Errorf(...)mark failed, keep going
t.Fatalf(...)mark failed, stop this test
t.Log/Logfrecord output (shown with -v or on failure)
t.Helper()report failures at the caller’s line
t.Run(name, fn)a named subtest
t.Parallel()run this test alongside other parallel ones
t.Cleanup(fn)deferred teardown (composable)
t.TempDir()auto-removed temp directory
t.Setenv(k, v)scoped environment variable
t.Skip(...)skip the rest of the test

Run with the race detector during development — go test -race ./... — to catch concurrency bugs the assertions alone won’t (see the race detector).

See also

Next: scaling one test into many cases — table-driven tests & subtests.

Check your understanding

Score: 0 / 5

1. What's the difference between t.Errorf and t.Fatalf?

Errorf marks the test failed but lets the rest of the function run, so you see every failing assertion. Fatalf marks it failed and calls runtime.Goexit, so nothing after it executes — use it when continuing makes no sense (e.g. a nil you'd dereference).

2. Where must a test for package math live, and what must the file be named?

go test compiles files ending in _test.go alongside the package. The function signature must be func TestXxx(t *testing.T) with an exported-style name after Test.

3. What does t.Helper() do?

Calling t.Helper() at the top of an assertion helper tells the testing package to skip that frame when printing the failure location, so the error points at the real test line that called it.

4. What does t.Cleanup(fn) do that's better than a bare defer in the test?

t.Cleanup registers teardown that runs in LIFO order when the test (and any subtests) complete — including after t.Fatal/Goexit. Unlike defer, it can be registered from inside a setup helper, so a helper owns both its setup and teardown.

5. How do you get a temp directory that's removed automatically when the test ends?

t.TempDir() creates a fresh directory unique to the test and registers cleanup to remove it. Each test (and subtest) gets its own, so file-based tests stay isolated with zero manual teardown.

Comments

Sign in with GitHub to join the discussion.