Testing and Mocking in Go
Testing is a first-class concern in Go. The standard library includes a powerful testing framework, a built-in benchmark runner, and a fuzzing engine — no third-party tools required. This guide covers everything from basic unit tests to advanced mocking patterns.
The testing Package
Go’s testing package provides the foundation. Test files follow the naming convention *_test.go and are excluded from production builds by the compiler.
Basic Unit Tests
// math.go
package math
func Add(a, b int) int {
return a + b
---
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(2, 3) = %d; want %d", result, expected)
}
---Run tests with:
go test # current package
go test ./... # all packages
go test -v ./... # verbose output
go test -run TestAdd # specific test
go test -count=1 ./... # disable cachingTest functions must have the signature func TestXxx(t *testing.T). The *testing.T parameter provides methods for logging, error reporting, and test control.
Table-Driven Tests
Table-driven tests are the idiomatic Go pattern for testing multiple cases:
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -1, -1, -2},
{"zero values", 0, 0, 0},
{"mixed signs", -5, 10, 5},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
}
})
}
---Table-driven tests are preferred over duplicated test functions because they make edge cases visible, reduce boilerplate, and make it easy to add new cases.
Setup and Teardown
func TestDatabase(t *testing.T) {
db := setupDatabase(t)
t.Cleanup(func() {
db.Close()
})
// run tests...
---t.Cleanup registers a function to run when the test completes or fails. This is cleaner than defer in tests because it works correctly with t.Parallel().
Test Helpers
Use t.Helper() to mark helper functions, which excludes them from failure stack traces:
func assertEqual(t *testing.T, got, want any) {
t.Helper()
if got != want {
t.Fatalf("got %v, want %v", got, want)
}
---
func TestAdd(t *testing.T) {
assertEqual(t, Add(2, 3), 5)
assertEqual(t, Add(10, -5), 5)
---Without t.Helper(), a failure would point to the assertEqual line. With it, the failure points to the actual test function line — much more useful during debugging.
Mocking with Interfaces
Go’s implicit interface satisfaction makes mocking straightforward. The standard pattern is:
- Define an interface for the dependency
- Write a mock implementation
- Inject the mock in tests
// mailer/mailer.go
package mailer
type Mailer interface {
Send(to, subject, body string) error
---
type SMTPMailer struct {
host string
port int
---
func (m *SMTPMailer) Send(to, subject, body string) error {
// real SMTP implementation
return nil
---
// handler/handler.go
package handler
type Handler struct {
mailer mailer.Mailer
---
func NewHandler(m mailer.Mailer) *Handler {
return &Handler{mailer: m}
---
func (h *Handler) NotifyUser(email string) error {
return h.mailer.Send(email, "Welcome!", "Thanks for signing up")
---Mock Implementation
// handler/handler_test.go
package handler
type mockMailer struct {
sentTo []string
---
func (m *mockMailer) Send(to, subject, body string) error {
m.sentTo = append(m.sentTo, to)
return nil
---
func TestNotifyUser(t *testing.T) {
mailer := &mockMailer{}
h := NewHandler(mailer)
h.NotifyUser("user@example.com")
if len(mailer.sentTo) != 1 {
t.Fatalf("expected 1 email, got %d", len(mailer.sentTo))
}
if mailer.sentTo[0] != "user@example.com" {
t.Errorf("expected user@example.com, got %s", mailer.sentTo[0])
}
---Testing HTTP Handlers
func TestHandlerGet(t *testing.T) {
mockStore := &mockStore{data: map[int]string{1: "item1"}}
h := NewHandler(mockStore)
req := httptest.NewRequest("GET", "/items/1", nil)
rec := httptest.NewRecorder()
h.GetItem(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("expected 200, got %d", rec.Code)
}
var resp map[string]string
json.NewDecoder(rec.Body).Decode(&resp)
if resp["name"] != "item1" {
t.Errorf("expected item1, got %s", resp["name"])
}
---The httptest package provides NewRequest, NewRecorder, and NewServer — everything needed to test HTTP handlers without network calls.
Testing with Testify
While the standard library is sufficient, testify adds convenient assertions and mocking:
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
type MockStore struct {
mock.Mock
---
func (m *MockStore) Get(id int) (string, error) {
args := m.Called(id)
return args.String(0), args.Error(1)
---
func TestGetItem(t *testing.T) {
mockStore := new(MockStore)
mockStore.On("Get", 1).Return("item1", nil)
result, err := mockStore.Get(1)
assert.NoError(t, err)
assert.Equal(t, "item1", result)
mockStore.AssertExpectations(t)
---Testify’s assert package produces better error messages than raw if checks and the mock package provides strict expectation verification.
Benchmarks
Benchmark functions use *testing.B and measure performance:
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(i, i+1)
}
---go test -bench=. -benchmem
go test -bench=BenchmarkAdd -count=5 ./...The b.N is adjusted by the framework to get a stable measurement. Always loop through b.N — never use a fixed iteration count.
Useful Benchmark Patterns
// Benchmark different input sizes
func BenchmarkAddSmall(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1, 2)
}
---
func BenchmarkAddLarge(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(1000000, 2000000)
}
---
// Reset timer after setup
func BenchmarkExpensive(b *testing.B) {
data := generateLargeDataset()
b.ResetTimer()
for i := 0; i < b.N; i++ {
process(data)
}
---Fuzzing
Go 1.18+ includes built-in fuzzing. Fuzz tests automatically generate inputs to find edge cases:
func FuzzReverse(f *testing.F) {
testcases := []string{"hello", "world", "12345"}
for _, tc := range testcases {
f.Add(tc) // seed corpus
}
f.Fuzz(func(t *testing.T, s string) {
reversed := Reverse(s)
doubleReversed := Reverse(reversed)
if s != doubleReversed {
t.Errorf("Reverse(Reverse(%q)) != %q", s, s)
}
})
---go test -fuzz=FuzzReverseThe fuzzer runs until it finds a crash or is interrupted. Found inputs are saved to testdata/fuzz/FuzzReverse/ and automatically replayed in future runs.
Integration Tests
For tests that need external services, use build tags to separate them:
// integration_test.go
//go:build integration
package store
import (
"testing"
)
func TestPostgresStore(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
// integration tests...
---go test -tags=integration ./...Use TEST_DATABASE_URL or similar environment variables to point to test instances. The testcontainers-go library can spin up disposable Docker containers for true integration testing.
Test Main
Use TestMain for package-level setup/teardown:
func TestMain(m *testing.M) {
setup()
code := m.Run()
teardown()
os.Exit(code)
---TestMain runs before any test in the package. Use it sparingly — it is often better to keep setup in individual test functions with t.Cleanup.
Test Coverage
go test -cover ./...
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out # browser view
go tool cover -func=coverage.out # per-function breakdownCoverage is a useful signal but not a goal. Focus on covering critical paths, error handling, and edge cases rather than chasing 100% coverage. The Pareto principle applies: 80% of bugs come from 20% of the code.
Golden File Testing
Golden file tests compare output against approved files (useful for complex output like HTML, JSON, or CLI output):
func TestHTMLOutput(t *testing.T) {
result := generateHTML()
golden := filepath.Join("testdata", t.Name()+".golden")
if *update {
os.WriteFile(golden, []byte(result), 0644)
}
expected, err := os.ReadFile(golden)
if err != nil {
t.Fatalf("failed to read golden file: %v", err)
}
if result != string(expected) {
t.Errorf("output doesn't match golden file")
}
---Golden files should be committed to version control and reviewed alongside code changes. When output intentionally changes, run go test -update to regenerate them, then review the diff in git.
Testing with Test Containers
For integration tests that need real databases or services:
import (
"testing"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
)
func TestWithPostgres(t *testing.T) {
ctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "postgres:16",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_USER": "test",
"POSTGRES_PASSWORD": "test",
"POSTGRES_DB": "testdb",
},
WaitingFor: wait.ForLog("database system is ready to accept connections"),
}
pg, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
t.Fatal(err)
}
defer pg.Terminate(ctx)
// Run tests against the container
db := connectToPG(pg)
// ... test logic
---FAQ
Q: How do I test code that uses time?
A: Inject a clock interface or use time.Now in a way that can be overridden in tests. The github.com/benbjohnson/clock package provides a testable clock.
Q: What is the difference between t.Error and t.Fatal?
A: t.Error reports a failure but continues the test. t.Fatal stops the test immediately. Use t.Error when you want to report multiple failures; use t.Fatal when continuing makes no sense.
Q: How do I test concurrent code?
A: Use sync.WaitGroup and timeouts. The race detector (go test -race) catches data races. For deterministic concurrency tests, use channels with select and timeouts.
Q: Can I run tests in parallel?
A: Yes, call t.Parallel() at the start of a test function. Tests sharing resources need synchronization. Parallel execution is controlled by -parallel N.
Q: How do I test a main package?
A: Create main_test.go in the same directory. Use func TestMain(m *testing.M) for the entry point, but test individual functions rather than the whole program.
Q: What is the best mocking library for Go?
A: The standard library’s net/http/httptest and manual interface implementations suffice for most projects. Testify’s mock package is popular for complex scenarios. gomock generates mocks from interfaces.
Q: How do I test file I/O?
A: Use os.CreateTemp to create temp files, or abstract the filesystem behind an io.Reader/io.Writer interface. The testing/fstest package provides a test filesystem.
For a comprehensive overview, read our article on Getting Started With Go.
For a comprehensive overview, read our article on Go Concurrency Guide.