Go Testing Basics
10 examples to get you started with Testing & Fuzzing - 7 basic and 3 intermediate.
Prerequisites
- Install Go 1.26.x or later from go.dev/dl.
- Create a module:
mkdir testing-demo && cd testing-demo && go mod init example.com/testing-demo. - No extra test runner is required;
go testis built into the toolchain.
go version
go test ./...Basic Examples
1. Your first Test function
A test is an ordinary function named TestXxx that receives *testing.T.
package mathutil
import "testing"
func Add(a, b int) int { return a + b }
func TestAdd(t *testing.T) {
if got := Add(2, 3); got != 5 {
t.Fatalf("Add(2,3) = %d, want 5", got)
}
}t.Fatalfmarks the test failed and stops the current function.- Test files end in
_test.goand live beside production code. - Run with
go testfrom the module root.
Related: Go Testing Culture: Simple, Fast, Table-Driven - why Go tests look like this
2. Run tests for one package
go test accepts package patterns like production builds.
go test ./mathutil
go test -v ./...-vprints each test name as it runs../...walks all packages under the module.- Exit code is non-zero when any test fails - ideal for CI.
Related: Coverage, Race Detector & Golden Files - flags beyond plain
go test
3. Table-driven cases
Collect inputs and expectations in a slice, then loop.
func TestAddTable(t *testing.T) {
tests := []struct {
a, b, want int
}{
{1, 2, 3},
{0, 0, 0},
{-1, 1, 0},
}
for _, tc := range tests {
if got := Add(tc.a, tc.b); got != tc.want {
t.Fatalf("Add(%d,%d) = %d, want %d", tc.a, tc.b, got, tc.want)
}
}
}- Adding a case is one struct literal, not a new function.
- Keep field names short but meaningful (
want,input,err). - Tables are the default pattern across the Go ecosystem.
Related: Table-Driven Tests & Subtests -
t.Runand shared setup
4. Subtests with t.Run
Subtests name each case in failure output.
func TestAddSub(t *testing.T) {
for _, tc := range []struct {
name string
a, b, want int
}{
{"positives", 1, 2, 3},
{"zeros", 0, 0, 0},
} {
t.Run(tc.name, func(t *testing.T) {
if got := Add(tc.a, tc.b); got != tc.want {
t.Fatalf("got %d want %d", got, tc.want)
}
})
}
}- Filter one case:
go test -run TestAddSub/zeros. - Subtests can call
t.Parallel()for concurrent cases. - Always give
namea distinct string per row.
Related: Table-Driven Tests & Subtests - parallel tables and setup helpers
5. t.Helper for shared assertions
Mark helper functions so failures report the caller's line.
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Fatalf("got %d want %d", got, want)
}
}
func TestWithHelper(t *testing.T) {
assertEqual(t, Add(1, 1), 2)
}- Without
t.Helper(), failures point inside the helper, not the test. - Helpers keep tables readable without duplicating
if got != want. - Helpers can live in
export_test.goor atestingsubpackage for reuse.
6. t.Error vs t.Fatal
Error records failure and continues; Fatal stops the current test function.
func TestMultiCheck(t *testing.T) {
if Add(1, 1) != 2 {
t.Error("add failed")
}
if Add(2, 2) != 4 {
t.Fatal("second check failed")
}
t.Log("only runs if Fatal did not fire")
}- Use
Fatalwhen later assertions assume earlier state. Logoutput appears with-vor on failure.- Subtests isolate failures better than many
Errorcalls in one function.
7. External test package
package foo_test imports the module as consumers do.
// in add_ext_test.go
package mathutil_test
import (
"testing"
"example.com/testing-demo/mathutil"
)
func TestAddExported(t *testing.T) {
if mathutil.Add(3, 4) != 7 {
t.Fatal("unexpected sum")
}
}- External tests only access exported identifiers.
- They document the public API surface.
- Internal tests (
package mathutil) can touch unexported helpers.
Related: Examples as Executable Documentation - godoc examples in test files
Intermediate Examples
8. httptest for HTTP handlers
net/http/httptest records responses without opening a real port.
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
)
func TestHelloHandler(t *testing.T) {
h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "ok")
})
req := httptest.NewRequest(http.MethodGet, "/", nil)
rec := httptest.NewRecorder()
h.ServeHTTP(rec, req)
if rec.Code != http.StatusOK || rec.Body.String() != "ok" {
t.Fatalf("got %d %q", rec.Code, rec.Body.String())
}
}NewRecordercaptures status, headers, and body.- No network means fast, deterministic handler tests.
- Pair with routers like chi, gin, or echo the same way.
Related: testify, httptest & Test Doubles - assertions and interface fakes
9. testify assertions (optional dependency)
testify shortens comparison failures with readable diffs.
go get github.com/stretchr/testify@latestimport (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWithTestify(t *testing.T) {
require.Equal(t, 4, Add(2, 2))
assert.NotEqual(t, 0, Add(1, 2))
}requirestops the test on failure liket.Fatal.assertcontinues liket.Error.- Teams often allow testify while keeping table-driven layout.
Related: testify, httptest & Test Doubles - mocks and suites
10. Benchmark and race smoke checks
Benchmarks and -race extend the same go test command.
import "testing"
func BenchmarkAdd(b *testing.B) {
for b.Loop() {
Add(1, 2)
}
}go test -bench=. -benchmem ./mathutil
go test -race ./...b.Loop()(Go 1.24+) replaces manualb.Nloops and resets timers correctly.-raceslows tests but catches data races in goroutine code.- Run race builds in CI, not on every keystroke.
Related: Benchmarks with testing.B & B.Loop - comparing implementations | Fuzz Testing with testing.F - seed corpora
Stack versions: This page was written for Go 1.26.x (Green Tea GC default, go fix modernizers - verify patch at build), chi (latest - verify at build), gin (latest - verify at build), echo (latest - verify at build), google.golang.org/grpc (latest - verify at build), sigs.k8s.io/controller-runtime (latest - verify at build), kubebuilder (latest - verify at build), tinygo (latest - verify board targets at build), wazero (latest - verify at build), and golangci-lint (latest - verify linter set at build).