Go Testing
Go Testing
Go ships a testing package with everything you need. No external framework required for basic tests. testify is the standard third-party addition.
go test ./... # run all tests
go test ./... -v # verbose output
go test -run TestFoo # run specific test
go test -count=1 # disable test caching
go test -cover ./... # coverage report
go test -bench=. -benchmem # run benchmarks with memory stats
go test -race ./... # race condition detector
Basic Test
// math_test.go ← must end in _test.go
package math
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)
}
}
- Test file:
*_test.go - Test function:
TestXxx(t *testing.T) t.Error/t.Errorf— mark fail, continuet.Fatal/t.Fatalf— mark fail, stop test immediately
Table-Driven Tests (standard Go pattern)
func TestAdd(t *testing.T) {
tests := []struct {
name string
a, b int
want int
}{
{"positive", 2, 3, 5},
{"negative", -1, -2, -3},
{"zero", 0, 0, 0},
{"overflow edge", math.MaxInt32, 1, math.MaxInt32 + 1},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
got := Add(tc.a, tc.b)
if got != tc.want {
t.Errorf("Add(%d, %d) = %d; want %d", tc.a, tc.b, got, tc.want)
}
})
}
}
t.Run creates subtests — each runs independently, failures are isolated.
testify — The Standard Addition
go get github.com/stretchr/testify
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestUser(t *testing.T) {
user, err := NewUser("Alice", "alice@example.com")
require.NoError(t, err) // fails test immediately if err != nil
assert.Equal(t, "Alice", user.Name)
assert.NotEmpty(t, user.ID)
assert.True(t, user.CreatedAt.Before(time.Now()))
}
require = stop on failure (like t.Fatal). Use for preconditions.
assert = continue on failure. Use for all other assertions.
Testing HTTP Handlers — httptest
import (
"net/http"
"net/http/httptest"
"testing"
"encoding/json"
)
func TestGetUser(t *testing.T) {
// setup
req := httptest.NewRequest("GET", "/users/1", nil)
w := httptest.NewRecorder()
// call handler directly
getUser(w, req)
// assert response
res := w.Result()
assert.Equal(t, http.StatusOK, res.StatusCode)
var user User
json.NewDecoder(res.Body).Decode(&user)
assert.Equal(t, "Alice", user.Name)
}
No need to start a real server. httptest.NewRecorder() captures the response.
Mocking with Interfaces
Go mocking = define an interface, swap the real impl with a fake.
// production code
type UserStore interface {
GetUser(ctx context.Context, id int) (User, error)
SaveUser(ctx context.Context, u User) error
}
type UserService struct {
store UserStore
}
// test fake
type fakeStore struct {
users map[int]User
}
func (f *fakeStore) GetUser(_ context.Context, id int) (User, error) {
u, ok := f.users[id]
if !ok {
return User{}, ErrNotFound
}
return u, nil
}
func (f *fakeStore) SaveUser(_ context.Context, u User) error {
f.users[u.ID] = u
return nil
}
// test
func TestGetUser(t *testing.T) {
store := &fakeStore{users: map[int]User{1: {ID: 1, Name: "Alice"}}}
svc := &UserService{store: store}
user, err := svc.GetUser(context.Background(), 1)
require.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
}
Benchmarks
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
go test -bench=BenchmarkAdd -benchmem -count=3
# BenchmarkAdd-8 1000000000 0.27 ns/op 0 B/op 0 allocs/op
b.Nis calibrated by Go to run long enough to be meaningful-benchmemshows heap allocations per op — key for optimizationb.ResetTimer()— exclude setup time from measurement
Test Helpers
// t.Helper() marks the function as a helper — error line points to caller, not helper
func assertUser(t *testing.T, got, want User) {
t.Helper()
assert.Equal(t, want.Name, got.Name)
assert.Equal(t, want.Email, got.Email)
}
Setup and Teardown
func TestMain(m *testing.M) {
// setup before all tests in this package
db = setupTestDB()
code := m.Run() // run all tests
// teardown after all tests
db.Close()
os.Exit(code)
}
For per-test setup use subtests + t.Cleanup:
func TestWithDB(t *testing.T) {
db := openTestDB(t)
t.Cleanup(func() { db.Close() }) // runs after test, even on failure
// use db...
}
Test Coverage
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out # open visual coverage in browser
go tool cover -func=coverage.out # per-function summary
Target: >70% for production services. 100% is often wasteful.
Race Detector
go test -race ./... # catches races at runtime
go run -race main.go # also works for running
Run CI with -race always. It's ~5-10x slower but catches real bugs.
Common Patterns
// parallel tests — faster suite
func TestSomething(t *testing.T) {
t.Parallel()
// ...
}
// skip slow tests in short mode
func TestSlowIntegration(t *testing.T) {
if testing.Short() {
t.Skip("skipping in short mode")
}
// expensive test...
}
// run: go test -short ./...
Interview Talking Points
- "Table-driven tests are the Go standard — one test function, slice of cases, t.Run for each. Failures are isolated and names are descriptive."
- "Mocking in Go = interface + fake implementation. No magic, no reflection-based mock framework needed."
- "httptest.NewRecorder() lets you test HTTP handlers without starting a server — just call the handler function directly."
- "Always run
go test -racein CI — the race detector catches real bugs that unit tests miss."
Related
- [[Go/HTTP Server]] — httptest for handler testing
- [[Go/Interfaces]] — interfaces enable mocking
- [[Go/Context]] — pass context.Background() in tests
- [[Go/Go Topics]] — full Go index