Back to Notes

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, continue
  • t.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.N is calibrated by Go to run long enough to be meaningful
  • -benchmem shows heap allocations per op — key for optimization
  • b.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 -race in 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