Testing Strategies in Go
Write effective Go tests — table-driven tests, subtests, test helpers, mocking with interfaces, benchmarks, and integration testing patterns.
What you'll learn
- ✓Table-driven tests and subtests
- ✓Test helpers and cleanup with t.Helper and t.Cleanup
- ✓Mocking with interfaces
- ✓Benchmarks and integration test patterns
Prerequisites
- •Go basics (functions, structs, interfaces)
- •Familiarity with the testing package
Go ships with a powerful testing framework in the standard library. No external dependencies needed. This guide covers the patterns that make Go tests readable, fast, and maintainable.
Table-driven tests
The most idiomatic Go testing pattern. Define test cases as a slice of structs and loop over them.
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -2, -3},
{"zero", 0, 0, 0},
{"mixed", -1, 5, 4},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Add(tt.a, tt.b)
if got != tt.expected {
t.Errorf("Add(%d, %d) = %d, want %d", tt.a, tt.b, got, tt.expected)
}
})
}
}
t.Run creates subtests. Each runs independently, has its own name, and can be filtered with -run.
go test -run TestAdd/negative
Test helpers
Mark helper functions with t.Helper() so failures report the caller’s line, not the helper’s.
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
Setup and cleanup
func TestDatabase(t *testing.T) {
db := setupTestDB(t)
t.Cleanup(func() {
db.Close()
})
// tests use db...
}
t.Cleanup runs after the test (and all its subtests) finish, even if the test fails.
Mocking with interfaces
Go does not need a mocking framework. Define a small interface and provide a test implementation.
type EmailSender interface {
Send(to, subject, body string) error
}
type mockSender struct {
calls []struct{ to, subject, body string }
}
func (m *mockSender) Send(to, subject, body string) error {
m.calls = append(m.calls, struct{ to, subject, body string }{to, subject, body})
return nil
}
func TestNotify(t *testing.T) {
sender := &mockSender{}
svc := NewService(sender)
svc.NotifyUser("alice@example.com", "Welcome!")
if len(sender.calls) != 1 {
t.Fatalf("expected 1 email, got %d", len(sender.calls))
}
if sender.calls[0].to != "alice@example.com" {
t.Errorf("sent to %q", sender.calls[0].to)
}
}
Benchmarks
func BenchmarkFibonacci(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(20)
}
}
Run with go test -bench=.. The framework adjusts b.N to get stable timing.
BenchmarkFibonacci-8 32456 36842 ns/op
Benchmark with setup
func BenchmarkSort(b *testing.B) {
data := generateRandomSlice(10000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
sorted := make([]int, len(data))
copy(sorted, data)
sort.Ints(sorted)
}
}
TestMain
Control setup and teardown for an entire test file.
func TestMain(m *testing.M) {
setup()
code := m.Run()
teardown()
os.Exit(code)
}
Integration tests with build tags
Separate slow integration tests with build tags.
//go:build integration
package myapp
func TestAPIIntegration(t *testing.T) {
// hits real API
}
Run only integration tests:
go test -tags=integration ./...
Golden files
Compare output against a known-good file.
func TestRender(t *testing.T) {
got := Render(input)
golden := filepath.Join("testdata", t.Name()+".golden")
if *update {
os.WriteFile(golden, []byte(got), 0644)
}
want, _ := os.ReadFile(golden)
if got != string(want) {
t.Errorf("output mismatch — run with -update to regenerate")
}
}
Parallel tests
func TestParallel(t *testing.T) {
tests := []struct{ name string }{{"a"}, {"b"}, {"c"}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// test runs concurrently with other parallel subtests
})
}
}
Summary
Go testing is simple by design. Table-driven tests give you coverage and readability. Interfaces give you mocking without frameworks. Benchmarks are built in. Build tags separate fast unit tests from slow integration tests. The standard library is all you need.
Related articles
- Go Dependency Injection Patterns in Go
Learn practical dependency injection in Go using interfaces, constructor injection, and functional options without heavy frameworks.
- Go Go Fuzz Testing Tutorial
Write fuzz tests in Go to discover edge cases and bugs automatically. Learn the fuzzing API, corpus management, and fixing found crashes.
- Go Table-Driven Tests and Test Helpers in Go
Write clear, maintainable Go tests using table-driven patterns, test helpers, subtests, and cleanup functions.
- Go The context Package in Go
Master Go's context package — cancellation, timeouts, deadlines, values, and how to propagate them through your application stack.