| name | go-testing-coverage |
| description | Go testing patterns, coverage analysis, and best practices. When writing tests for Go code or analyzing test coverage |
| metadata | {"languages":"go"} |
Go Testing & Coverage
Testing patterns and coverage analysis for Go projects.
Reference URLs
For deeper information, fetch these URLs:
Basic Testing
Test File Structure
Tests live in *_test.go files alongside the code:
mypackage/
├── mycode.go
└── mycode_test.go
Simple Test
package mypackage
import "testing"
func TestFunctionName(t *testing.T) {
input := "test"
want := "expected"
got := FunctionName(input)
if got != want {
t.Errorf("FunctionName(%q) = %q; want %q", input, got, want)
}
}
Error Message Format
Follow Go convention: actual != expected, message matches order.
if got != want {
t.Errorf("FunctionName(%q) = %d; want %d", input, got, want)
}
if want != got {
t.Errorf("expected %d, got %d", want, got)
}
Table-Driven Tests
Preferred pattern for multiple test cases:
func TestReverseRunes(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{"simple", "Hello", "olleH"},
{"unicode", "Hello, 世界", "界世 ,olleH"},
{"empty", "", ""},
{"single", "a", "a"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := ReverseRunes(tt.in)
if got != tt.want {
t.Errorf("ReverseRunes(%q) = %q; want %q", tt.in, got, tt.want)
}
})
}
}
Parallel Tests
func TestReverseRunes(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{"simple", "Hello", "olleH"},
{"unicode", "Hello, 世界", "界世 ,olleH"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := ReverseRunes(tt.in)
if got != tt.want {
t.Errorf("ReverseRunes(%q) = %q; want %q", tt.in, got, tt.want)
}
})
}
}
Running Tests
Basic Commands
go test ./...
go test
go test -run TestFunctionName
go test -run TestReverseRunes/simple
go test -v ./...
go test -short ./...
Race Detection
go test -race ./...
Benchmarks
go test -bench=. ./...
go test -bench=BenchmarkFunctionName ./...
go test -bench=. -benchmem ./...
Coverage Analysis
Basic Coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
Coverage Modes
go test -covermode=count -coverprofile=coverage.out ./...
go test -covermode=set -coverprofile=coverage.out ./...
go test -covermode=atomic -coverprofile=coverage.out ./...
Integration Test Coverage
go build -cover -o myapp ./cmd/myapp
GOCOVERDIR=./coverage ./myapp
go tool covdata textfmt -i=./coverage -o coverage.out
Test Helpers
Setup and Teardown
func TestMain(m *testing.M) {
setup()
code := m.Run()
teardown()
os.Exit(code)
}
Helper Functions
func TestSomething(t *testing.T) {
helper := setupHelper(t)
defer helper.cleanup()
}
func setupHelper(t *testing.T) *TestHelper {
t.Helper()
return &TestHelper{}
}
Temporary Files
func TestWithTempFile(t *testing.T) {
dir := t.TempDir()
f, err := os.CreateTemp(dir, "test-*.txt")
if err != nil {
t.Fatal(err)
}
defer f.Close()
}
Benchmarking
Basic Benchmark
func BenchmarkReverseRunes(b *testing.B) {
input := "Hello, World!"
for i := 0; i < b.N; i++ {
ReverseRunes(input)
}
}
Benchmark with Setup
func BenchmarkReverseRunes(b *testing.B) {
input := "Hello, World!"
b.ResetTimer()
for i := 0; i < b.N; i++ {
ReverseRunes(input)
}
}
Sub-benchmarks
func BenchmarkReverseRunes(b *testing.B) {
cases := []struct {
name string
in string
}{
{"short", "Hello"},
{"medium", "Hello, World! How are you today?"},
{"long", strings.Repeat("Hello, World!", 100)},
}
for _, tc := range cases {
b.Run(tc.name, func(b *testing.B) {
for i := 0; i < b.N; i++ {
ReverseRunes(tc.in)
}
})
}
}
Test Patterns
Testing Errors
func TestFunctionError(t *testing.T) {
_, err := FunctionThatMightFail(invalidInput)
if err == nil {
t.Fatal("expected error, got nil")
}
var myErr *MyError
if !errors.As(err, &myErr) {
t.Errorf("expected MyError, got %T", err)
}
}
Testing HTTP Handlers
func TestHandler(t *testing.T) {
req := httptest.NewRequest("GET", "/path", nil)
w := httptest.NewRecorder()
handler(w, req)
resp := w.Result()
if resp.StatusCode != http.StatusOK {
t.Errorf("status = %d; want %d", resp.StatusCode, http.StatusOK)
}
}
Testing with Context
func TestWithContext(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
result, err := FunctionWithContext(ctx)
if err != nil {
t.Fatal(err)
}
}
External Test Package
Use _test suffix for black-box testing:
package mypackage_test
import (
"testing"
"github.com/user/project/mypackage"
)
func TestPublicAPI(t *testing.T) {
result := mypackage.PublicFunction()
}
Quick Reference
Commands
| Command | Purpose |
|---|
go test | Run tests |
go test -v | Verbose output |
go test -race | Race detection |
go test -cover | Coverage summary |
go test -coverprofile=c.out | Coverage profile |
go tool cover -html=c.out | HTML report |
go test -bench=. | Run benchmarks |
Coverage Targets
- 80%+ for critical paths
- 60%+ for utilities
- Focus on meaningful coverage, not just numbers
Modern Testing Patterns (Go 1.21+)
Documentation: https://go.dev/blog/testing-time
Using slices in Tests
The slices package simplifies test assertions:
func TestSort(t *testing.T) {
got := MySort([]int{3, 1, 4, 1, 5})
want := []int{1, 1, 3, 4, 5}
if !slices.Equal(got, want) {
t.Errorf("MySort() = %v; want %v", got, want)
}
if !slices.Contains(got, 3) {
t.Error("expected result to contain 3")
}
}
Generic Test Helpers
Write type-safe test helpers using generics:
func assertEqual[T comparable](t *testing.T, got, want T) {
t.Helper()
if got != want {
t.Errorf("got %v; want %v", got, want)
}
}
func assertSliceEqual[T comparable](t *testing.T, got, want []T) {
t.Helper()
if !slices.Equal(got, want) {
t.Errorf("got %v; want %v", got, want)
}
}
testing/slogtest
For testing log/slog handlers:
import "testing/slogtest"
func TestHandler(t *testing.T) {
var buf bytes.Buffer
h := slog.NewJSONHandler(&buf, nil)
results := func() []map[string]any {
}
if err := slogtest.TestHandler(h, results); err != nil {
t.Error(err)
}
}
References:
Test Code Quality
Avoiding Dead Code in Tests
Dead code in tests causes unnecessary review cycles. Before marking tests complete:
- No unused variables: Remove any variable declared but never used
- No discarded values: Never assign to
_ unless explicitly ignoring an error
- No unnecessary imports: Remove unused imports (especially
sync/atomic when not needed)
- Meaningful assertions: Replace trivially-passing assertions with checks that verify actual behavior
Example of bad code (from session):
triggerCount := &atomic.Int32{}
originalRun := d.run
Example of good code:
if !d.timer.Stop() {
<-d.timer.C
}
Common Mistakes
- Testing implementation - Test behavior, not internals
- Unhelpful errors - Include inputs, expected, actual
- No edge cases - Empty, nil, boundary values
- Flaky tests - Avoid time-dependent tests (use clock injection)
- Too many mocks - Test real code when possible
- Coverage obsession - Quality > quantity
- Manual slice comparison - Use
slices.Equal instead