A test kicks off a goroutine to check something asynchronously, then returns right away without waiting for it — the goroutine is still sleeping when the test function exits. The test itself reports PASS. Then, moments later, from inside a completely different, unrelated test that happens to still be running, the whole suite crashes with a panic that has nothing to do with that test at all.
func TestAsync(t *testing.T) {
go func() {
time.Sleep(50 * time.Millisecond)
t.Errorf("work failed") // goroutine outlives the test function
}()
// TestAsync returns immediately, before the goroutine finishes
}
// --- PASS: TestAsync (0.00s) <- looks fine at first...
// panic: Fail in goroutine after TestAsync has completed
//
// goroutine 7 [running]:
// testing.(*common).Errorf(...)
// asynctest.TestAsync.func1()
// async_test.go:11 +0x45
// FAILGo testing needs no third-party framework: any function named TestXxx (taking a single *testing.T parameter) inside a file named *_test.go is automatically discovered and run by go test. The same mechanism extends to benchmarks (BenchmarkXxx, taking *testing.B) and example tests that double as documentation.
A basic test
A test file lives alongside the code it tests, named xxx_test.go. It calls t.Errorf (or t.Fatalf to stop immediately) to report a failure — there's no assertion library built in, though many projects add one.
package mathutil
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 with go test in the package directory, or go test ./... to test every package in a module.
This is also where the bug in the callout above comes from: *testing.T is only safe to call from the test's own goroutine while that test is still running. If a test launches a goroutine and returns before the goroutine finishes, any later call to t.Errorf from it happens after the test has already been torn down — which the testing package treats as a hard panic, not a quiet failure, and it can surface while a totally different test is running. The fix is to make the test actually wait for its own goroutine before returning:
func TestAsync(t *testing.T) {
done := make(chan struct{})
go func() {
defer close(done)
time.Sleep(50 * time.Millisecond)
t.Log("work finished inside the test's lifetime")
}()
<-done // block until the goroutine is done before the test returns
}
// --- PASS: TestAsync (0.05s)Table-driven tests
The idiomatic way to test many input/output cases without repeating code is a table-driven test: a slice of structs describing each case, looped over with t.Run to name and isolate each one.
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
expected int
}{
{"positive", 2, 3, 5},
{"negative", -1, -1, -2},
{"zero", 0, 0, 0},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := Add(tc.a, tc.b); got != tc.expected {
t.Errorf("got %d, want %d", got, tc.expected)
}
})
}
}Benchmarks
A function named BenchmarkXxx taking *testing.B and looping b.N times measures performance; go test -bench=. runs it and reports time per operation, with the framework automatically adjusting b.N until the measurement is statistically stable.
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}