- Authors

- Name
- Youngju Kim
- @fjvbn20031
Introduction
Software quality is proven through tests. Go ships with powerful testing tools built into the language itself — the testing package and go test command alone are enough to write production-grade tests.
This guide covers practical, immediately applicable patterns from basic unit tests to dependency isolation with mocks and stubs, all the way through integration testing.
1. Go Testing Basics
1.1 The testing Package
Go tests are written in _test.go files, and test functions start with the Test prefix.
// calculator.go
package calculator
import "fmt"
func Add(a, b int) int { return a + b }
func Sub(a, b int) int { return a - b }
func Mul(a, b int) int { return a * b }
func Div(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
// calculator_test.go
package calculator
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)
}
}
// t.Fatal: stops execution of the current test immediately
func TestDiv(t *testing.T) {
result, err := Div(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != 5.0 {
t.Errorf("Div(10, 2) = %f; want 5.0", result)
}
// Test division by zero
_, err = Div(10, 0)
if err == nil {
t.Error("Div by zero should return an error")
}
}
// t.Helper(): marks a function as a test helper so error lines point to the caller
func assertEqual(t *testing.T, got, want int) {
t.Helper()
if got != want {
t.Errorf("got %d, want %d", got, want)
}
}
func TestMul(t *testing.T) {
assertEqual(t, Mul(3, 4), 12)
assertEqual(t, Mul(0, 100), 0)
assertEqual(t, Mul(-2, 5), -10)
}
1.2 Running Tests
# Test current package
go test
# Test all packages
go test ./...
# Verbose output
go test -v
# Run only specific tests (regex)
go test -run TestAdd
go test -run TestDiv
# Coverage
go test -cover
go test -coverprofile=coverage.out
go tool cover -html=coverage.out # open in browser
go tool cover -func=coverage.out # per-function coverage
# Set timeout
go test -timeout 30s
# Detect race conditions
go test -race
# Disable caching
go test -count=1 ./...
1.3 TestMain
package mypackage_test
import (
"fmt"
"os"
"testing"
)
var testDB *Database
// TestMain: runs setup and teardown around the entire test suite
func TestMain(m *testing.M) {
// Setup
fmt.Println("Setting up test environment")
testDB = setupTestDatabase()
// Run tests
code := m.Run()
// Teardown
testDB.Close()
fmt.Println("Test environment cleaned up")
os.Exit(code)
}
2. Table-Driven Tests
The most idiomatic testing pattern in Go.
package calculator_test
import (
"fmt"
"testing"
"github.com/example/calculator"
)
func TestAdd_TableDriven(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive + positive", 2, 3, 5},
{"positive + negative", 5, -3, 2},
{"negative + negative", -4, -6, -10},
{"zero case", 0, 0, 0},
{"large numbers", 1000000, 2000000, 3000000},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := calculator.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 test with error cases
func TestDiv_TableDriven(t *testing.T) {
tests := []struct {
name string
a, b float64
expected float64
expectErr bool
}{
{"normal division", 10, 2, 5.0, false},
{"fractional result", 7, 2, 3.5, false},
{"division by zero", 10, 0, 0, true},
{"negative dividend", -10, 2, -5.0, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := calculator.Div(tt.a, tt.b)
if tt.expectErr {
if err == nil {
t.Error("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result != tt.expected {
t.Errorf("Div(%.1f, %.1f) = %.1f; want %.1f",
tt.a, tt.b, result, tt.expected)
}
})
}
}
// Per-subtest setup and teardown
func TestUserService_TableDriven(t *testing.T) {
tests := []struct {
name string
userID int
wantErr bool
}{
{"valid user", 1, false},
{"user not found", 999, true},
{"negative ID", -1, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Per-subtest setup
svc := NewTestUserService()
defer svc.Cleanup()
user, err := svc.GetUser(tt.userID)
if (err != nil) != tt.wantErr {
t.Errorf("GetUser(%d) error = %v, wantErr %v",
tt.userID, err, tt.wantErr)
return
}
if !tt.wantErr {
fmt.Printf("user: %+v\n", user)
}
})
}
}
3. The testify Library
testify is the most widely used testing library in Go.
go get github.com/stretchr/testify
3.1 assert vs require
package mypackage_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestWithTestify(t *testing.T) {
result := Add(2, 3)
// assert: test continues even on failure
assert.Equal(t, 5, result, "2+3 should equal 5")
assert.NotEqual(t, 6, result)
assert.True(t, result > 0)
assert.False(t, result < 0)
// require: test stops immediately on failure
require.NotNil(t, result)
require.Equal(t, 5, result)
}
func TestCollections(t *testing.T) {
nums := []int{1, 2, 3, 4, 5}
assert.Contains(t, nums, 3)
assert.NotContains(t, nums, 10)
assert.Len(t, nums, 5)
assert.Empty(t, []int{})
assert.NotEmpty(t, nums)
m := map[string]int{"a": 1, "b": 2}
assert.Equal(t, 1, m["a"])
}
func TestErrors(t *testing.T) {
_, err := Div(10, 0)
assert.Error(t, err)
assert.EqualError(t, err, "division by zero")
result, err := Div(10, 2)
assert.NoError(t, err)
assert.Equal(t, 5.0, result)
}
func TestPanic(t *testing.T) {
assert.Panics(t, func() {
panic("test panic")
})
assert.NotPanics(t, func() {
_ = Add(1, 2)
})
}
3.2 assert.JSONEq and HTTP Assertions
package mypackage_test
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestJSONResponse(t *testing.T) {
handler := func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"name":"Alice","age":30}`))
}
req := httptest.NewRequest(http.MethodGet, "/user", nil)
rec := httptest.NewRecorder()
handler(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
// JSON comparison regardless of key order
expected := `{"age": 30, "name": "Alice"}`
assert.JSONEq(t, expected, rec.Body.String())
}
3.3 testify/suite
package mypackage_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
)
type UserServiceSuite struct {
suite.Suite
service *UserService
db *TestDB
}
// Runs once before the entire suite
func (s *UserServiceSuite) SetupSuite() {
s.db = NewTestDB()
}
// Runs before each test
func (s *UserServiceSuite) SetupTest() {
s.service = NewUserService(s.db)
s.db.Seed()
}
// Runs after each test
func (s *UserServiceSuite) TearDownTest() {
s.db.Clean()
}
// Runs once after the entire suite
func (s *UserServiceSuite) TearDownSuite() {
s.db.Close()
}
func (s *UserServiceSuite) TestGetUser() {
user, err := s.service.GetUser(1)
s.NoError(err)
s.Equal("Alice", user.Name)
}
func (s *UserServiceSuite) TestCreateUser() {
user, err := s.service.CreateUser("Bob", "bob@example.com")
s.NoError(err)
s.NotZero(user.ID)
s.Equal("Bob", user.Name)
}
func (s *UserServiceSuite) TestDeleteUser_NotFound() {
err := s.service.DeleteUser(9999)
s.Error(err)
assert.Contains(s.T(), err.Error(), "not found")
}
// Register the suite with the Go test runner
func TestUserServiceSuite(t *testing.T) {
suite.Run(t, new(UserServiceSuite))
}
4. Interface-Based Mocks
4.1 Dependency Injection via Interfaces
// user_repository.go
package user
import (
"context"
"fmt"
)
type User struct {
ID int
Name string
Email string
}
// Interface definition (the key to dependency injection)
type Repository interface {
FindByID(ctx context.Context, id int) (*User, error)
Save(ctx context.Context, user *User) error
Delete(ctx context.Context, id int) error
List(ctx context.Context) ([]*User, error)
}
type EmailSender interface {
Send(to, subject, body string) error
}
// Service depends on interfaces, not concrete implementations
type Service struct {
repo Repository
email EmailSender
}
func NewService(repo Repository, email EmailSender) *Service {
return &Service{repo: repo, email: email}
}
func (s *Service) GetUser(ctx context.Context, id int) (*User, error) {
return s.repo.FindByID(ctx, id)
}
func (s *Service) CreateUser(ctx context.Context, name, email string) (*User, error) {
user := &User{Name: name, Email: email}
if err := s.repo.Save(ctx, user); err != nil {
return nil, fmt.Errorf("saving user: %w", err)
}
// Send welcome email (failure doesn't affect user creation)
_ = s.email.Send(email, "Welcome!", "Thank you for signing up!")
return user, nil
}
4.2 Manual Mock Implementation
// Can be defined directly in test files
package user_test
import (
"context"
"fmt"
"github.com/example/user"
)
type MockRepository struct {
users map[int]*user.User
saveErr error
findErr error
}
func NewMockRepository() *MockRepository {
return &MockRepository{users: make(map[int]*user.User)}
}
func (m *MockRepository) FindByID(ctx context.Context, id int) (*user.User, error) {
if m.findErr != nil {
return nil, m.findErr
}
u, ok := m.users[id]
if !ok {
return nil, fmt.Errorf("user %d not found", id)
}
return u, nil
}
func (m *MockRepository) Save(ctx context.Context, u *user.User) error {
if m.saveErr != nil {
return m.saveErr
}
if u.ID == 0 {
u.ID = len(m.users) + 1
}
m.users[u.ID] = u
return nil
}
func (m *MockRepository) Delete(ctx context.Context, id int) error {
delete(m.users, id)
return nil
}
func (m *MockRepository) List(ctx context.Context) ([]*user.User, error) {
users := make([]*user.User, 0, len(m.users))
for _, u := range m.users {
users = append(users, u)
}
return users, nil
}
type MockEmailSender struct {
SentEmails []struct{ To, Subject, Body string }
SendErr error
}
func (m *MockEmailSender) Send(to, subject, body string) error {
if m.SendErr != nil {
return m.SendErr
}
m.SentEmails = append(m.SentEmails, struct{ To, Subject, Body string }{to, subject, body})
return nil
}
5. Using gomock
5.1 Installing gomock and Generating Mocks
go install go.uber.org/mock/mockgen@latest
go get go.uber.org/mock/gomock
Add a go:generate directive to your interface file:
// repository.go
package user
//go:generate mockgen -source=repository.go -destination=mocks/mock_repository.go -package=mocks
type Repository interface {
FindByID(ctx context.Context, id int) (*User, error)
Save(ctx context.Context, user *User) error
Delete(ctx context.Context, id int) error
List(ctx context.Context) ([]*User, error)
}
# Generate mocks
go generate ./...
5.2 Writing Tests with gomock
package user_test
import (
"context"
"fmt"
"testing"
"github.com/example/user"
"github.com/example/user/mocks"
"go.uber.org/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestGetUser_WithGomock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockRepository(ctrl)
mockEmail := mocks.NewMockEmailSender(ctrl)
expectedUser := &user.User{ID: 1, Name: "Alice", Email: "alice@example.com"}
// EXPECT: set up call expectations
mockRepo.EXPECT().
FindByID(gomock.Any(), 1).
Return(expectedUser, nil).
Times(1)
svc := user.NewService(mockRepo, mockEmail)
got, err := svc.GetUser(context.Background(), 1)
assert.NoError(t, err)
assert.Equal(t, expectedUser, got)
}
func TestCreateUser_WithGomock(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockRepository(ctrl)
mockEmail := mocks.NewMockEmailSender(ctrl)
// Save must be called exactly once
mockRepo.EXPECT().
Save(gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, u *user.User) error {
u.ID = 42 // simulate ID assignment
return nil
}).
Times(1)
// Send can be called zero or more times
mockEmail.EXPECT().
Send(gomock.Any(), gomock.Any(), gomock.Any()).
Return(nil).
AnyTimes()
svc := user.NewService(mockRepo, mockEmail)
got, err := svc.CreateUser(context.Background(), "Bob", "bob@example.com")
assert.NoError(t, err)
assert.Equal(t, 42, got.ID)
assert.Equal(t, "Bob", got.Name)
}
// Using specific argument matchers
func TestFindByID_SpecificArgs(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockRepository(ctrl)
// Exact argument matching
mockRepo.EXPECT().
FindByID(gomock.Any(), gomock.Eq(5)).
Return(nil, fmt.Errorf("not found")).
Times(1)
// Custom matcher
mockRepo.EXPECT().
FindByID(gomock.Any(), gomock.Not(gomock.Eq(0))).
Return(&user.User{ID: 10}, nil).
AnyTimes()
}
6. Using testify/mock
6.1 Implementing Mocks with testify/mock
package user_test
import (
"context"
"testing"
"github.com/example/user"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert"
)
// Mock struct embedding mock.Mock
type MockUserRepo struct {
mock.Mock
}
func (m *MockUserRepo) FindByID(ctx context.Context, id int) (*user.User, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*user.User), args.Error(1)
}
func (m *MockUserRepo) Save(ctx context.Context, u *user.User) error {
args := m.Called(ctx, u)
return args.Error(0)
}
func (m *MockUserRepo) Delete(ctx context.Context, id int) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func (m *MockUserRepo) List(ctx context.Context) ([]*user.User, error) {
args := m.Called(ctx)
return args.Get(0).([]*user.User), args.Error(1)
}
func TestGetUser_Testify(t *testing.T) {
mockRepo := new(MockUserRepo)
mockEmail := new(MockEmailSender)
expectedUser := &user.User{ID: 1, Name: "Alice"}
// Use On() to configure behavior
mockRepo.On("FindByID", mock.Anything, 1).
Return(expectedUser, nil)
svc := user.NewService(mockRepo, mockEmail)
got, err := svc.GetUser(context.Background(), 1)
assert.NoError(t, err)
assert.Equal(t, expectedUser, got)
// Verify all expectations were met
mockRepo.AssertExpectations(t)
}
func TestCreateUser_Testify(t *testing.T) {
mockRepo := new(MockUserRepo)
mockEmail := new(MockEmailSender)
// Use mock.MatchedBy for custom matchers
mockRepo.On("Save", mock.Anything, mock.MatchedBy(func(u *user.User) bool {
return u.Name == "Bob"
})).Return(nil)
mockEmail.On("Send", mock.Anything, mock.Anything, mock.Anything).Return(nil)
svc := user.NewService(mockRepo, mockEmail)
_, err := svc.CreateUser(context.Background(), "Bob", "bob@example.com")
assert.NoError(t, err)
mockRepo.AssertNumberOfCalls(t, "Save", 1)
mockEmail.AssertNumberOfCalls(t, "Send", 1)
}
7. HTTP Mock Server (httptest)
7.1 httptest.NewRecorder
package handler_test
import (
"bytes"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestUserHandler_GetAll(t *testing.T) {
store := NewInMemoryStore()
store.Add(&User{ID: 1, Name: "Alice"})
store.Add(&User{ID: 2, Name: "Bob"})
handler := NewUserHandler(store)
req := httptest.NewRequest(http.MethodGet, "/users", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Equal(t, "application/json", rec.Header().Get("Content-Type"))
var users []User
err := json.NewDecoder(rec.Body).Decode(&users)
assert.NoError(t, err)
assert.Len(t, users, 2)
}
func TestUserHandler_Create(t *testing.T) {
store := NewInMemoryStore()
handler := NewUserHandler(store)
body := map[string]string{
"name": "Charlie",
"email": "charlie@example.com",
}
jsonBody, _ := json.Marshal(body)
req := httptest.NewRequest(http.MethodPost, "/users",
bytes.NewBuffer(jsonBody))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusCreated, rec.Code)
var created User
json.NewDecoder(rec.Body).Decode(&created)
assert.Equal(t, "Charlie", created.Name)
assert.NotZero(t, created.ID)
}
7.2 httptest.NewServer (Mocking External APIs)
package client_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHTTPClient_GetUser(t *testing.T) {
// Test server that mocks an external service
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Validate the request
assert.Equal(t, "/api/users/1", r.URL.Path)
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
})
}))
defer ts.Close()
client := NewUserClient(ts.URL, "test-token")
user, err := client.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
}
func TestHTTPClient_ServerError(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}))
defer ts.Close()
client := NewUserClient(ts.URL, "test-token")
_, err := client.GetUser(1)
assert.Error(t, err)
}
func TestHTTPClient_MultipleEndpoints(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("/api/users", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode([]User{{ID: 1, Name: "Alice"}, {ID: 2, Name: "Bob"}})
})
mux.HandleFunc("/api/health", func(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
})
ts := httptest.NewServer(mux)
defer ts.Close()
client := NewUserClient(ts.URL, "token")
users, err := client.ListUsers()
assert.NoError(t, err)
assert.Len(t, users, 2)
}
8. Stub Patterns
8.1 Interface-Based Stubs
// Time Stub: making time.Now() replaceable
type Clock interface {
Now() time.Time
}
type RealClock struct{}
func (r RealClock) Now() time.Time { return time.Now() }
type FakeClock struct {
fakeTime time.Time
}
func (f FakeClock) Now() time.Time { return f.fakeTime }
// File System Stub
type FileSystem interface {
ReadFile(name string) ([]byte, error)
WriteFile(name string, data []byte, perm os.FileMode) error
Exists(name string) bool
}
type OSFileSystem struct{}
func (f OSFileSystem) ReadFile(name string) ([]byte, error) { return os.ReadFile(name) }
func (f OSFileSystem) WriteFile(name string, data []byte, perm os.FileMode) error {
return os.WriteFile(name, data, perm)
}
func (f OSFileSystem) Exists(name string) bool {
_, err := os.Stat(name)
return !os.IsNotExist(err)
}
// In-memory file system stub
type MemoryFileSystem struct {
files map[string][]byte
}
func NewMemoryFileSystem() *MemoryFileSystem {
return &MemoryFileSystem{files: make(map[string][]byte)}
}
func (m *MemoryFileSystem) ReadFile(name string) ([]byte, error) {
data, ok := m.files[name]
if !ok {
return nil, fmt.Errorf("file not found: %s", name)
}
return data, nil
}
func (m *MemoryFileSystem) WriteFile(name string, data []byte, perm os.FileMode) error {
m.files[name] = data
return nil
}
func (m *MemoryFileSystem) Exists(name string) bool {
_, ok := m.files[name]
return ok
}
// Service test using stubs
type ConfigService struct {
fs FileSystem
clock Clock
}
func (s *ConfigService) LoadConfig(path string) (*Config, error) {
data, err := s.fs.ReadFile(path)
if err != nil {
return nil, err
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, err
}
cfg.LoadedAt = s.clock.Now()
return &cfg, nil
}
8.2 Database Stub (sqlmock)
package repository_test
import (
"context"
"regexp"
"testing"
"github.com/DATA-DOG/go-sqlmock"
"github.com/stretchr/testify/assert"
)
func TestUserRepository_FindByID(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("failed to create sqlmock: %v", err)
}
defer db.Close()
rows := sqlmock.NewRows([]string{"id", "name", "email"}).
AddRow(1, "Alice", "alice@example.com")
mock.ExpectQuery(regexp.QuoteMeta("SELECT id, name, email FROM users WHERE id = ?")).
WithArgs(1).
WillReturnRows(rows)
repo := NewSQLUserRepository(db)
user, err := repo.FindByID(context.Background(), 1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
assert.NoError(t, mock.ExpectationsWereMet())
}
func TestUserRepository_Save(t *testing.T) {
db, mock, _ := sqlmock.New()
defer db.Close()
mock.ExpectExec(regexp.QuoteMeta(
"INSERT INTO users (name, email) VALUES (?, ?)")).
WithArgs("Bob", "bob@example.com").
WillReturnResult(sqlmock.NewResult(42, 1))
repo := NewSQLUserRepository(db)
user := &User{Name: "Bob", Email: "bob@example.com"}
err := repo.Save(context.Background(), user)
assert.NoError(t, err)
assert.Equal(t, 42, user.ID)
assert.NoError(t, mock.ExpectationsWereMet())
}
9. Building a Mock Server
// mockserver/server.go
package mockserver
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"sync"
"testing"
)
type RequestRecord struct {
Method string
Path string
Headers http.Header
Body []byte
}
type MockResponse struct {
Status int
Headers map[string]string
Body interface{}
}
type Route struct {
Method string
Path string
Response MockResponse
}
type MockServer struct {
t *testing.T
server *httptest.Server
routes []Route
requests []RequestRecord
mu sync.Mutex
}
func New(t *testing.T) *MockServer {
ms := &MockServer{t: t}
mux := http.NewServeMux()
mux.HandleFunc("/", ms.handleRequest)
ms.server = httptest.NewServer(mux)
t.Cleanup(ms.server.Close)
return ms
}
func (ms *MockServer) handleRequest(w http.ResponseWriter, r *http.Request) {
ms.mu.Lock()
defer ms.mu.Unlock()
body, _ := io.ReadAll(r.Body)
ms.requests = append(ms.requests, RequestRecord{
Method: r.Method,
Path: r.URL.Path,
Headers: r.Header.Clone(),
Body: body,
})
for _, route := range ms.routes {
if route.Method == r.Method && route.Path == r.URL.Path {
for k, v := range route.Response.Headers {
w.Header().Set(k, v)
}
if _, ok := route.Response.Headers["Content-Type"]; !ok {
w.Header().Set("Content-Type", "application/json")
}
w.WriteHeader(route.Response.Status)
if route.Response.Body != nil {
json.NewEncoder(w).Encode(route.Response.Body)
}
return
}
}
ms.t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
http.Error(w, "Not Found", http.StatusNotFound)
}
func (ms *MockServer) AddRoute(method, path string, resp MockResponse) *MockServer {
ms.mu.Lock()
defer ms.mu.Unlock()
ms.routes = append(ms.routes, Route{Method: method, Path: path, Response: resp})
return ms
}
func (ms *MockServer) URL() string {
return ms.server.URL
}
func (ms *MockServer) Requests() []RequestRecord {
ms.mu.Lock()
defer ms.mu.Unlock()
return append([]RequestRecord{}, ms.requests...)
}
func (ms *MockServer) AssertRequestCount(t *testing.T, method, path string, count int) {
t.Helper()
actual := 0
for _, r := range ms.Requests() {
if r.Method == method && r.Path == path {
actual++
}
}
if actual != count {
t.Errorf("%s %s: request count = %d, want %d", method, path, actual, count)
}
}
// Usage example
func TestWithMockServer(t *testing.T) {
ms := New(t)
ms.AddRoute("GET", "/api/users", MockResponse{
Status: 200,
Body: []map[string]interface{}{{"id": 1, "name": "Alice"}},
})
ms.AddRoute("POST", "/api/users", MockResponse{
Status: 201,
Body: map[string]interface{}{"id": 2, "name": "Bob"},
})
client := NewAPIClient(ms.URL())
users, err := client.GetUsers()
assert.NoError(t, err)
assert.Len(t, users, 1)
ms.AssertRequestCount(t, "GET", "/api/users", 1)
}
10. Benchmark Tests
package calculator_test
import (
"fmt"
"strings"
"testing"
)
// Benchmark: run with go test -bench=.
func BenchmarkAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(100, 200)
}
}
func BenchmarkFibonacci(b *testing.B) {
sizes := []int{10, 20, 30}
for _, n := range sizes {
n := n
b.Run(fmt.Sprintf("n=%d", n), func(b *testing.B) {
for i := 0; i < b.N; i++ {
Fibonacci(n)
}
})
}
}
// Track memory allocations
func BenchmarkStringConcat(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var s string
for j := 0; j < 100; j++ {
s += "x"
}
_ = s
}
}
func BenchmarkStringBuilder(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var sb strings.Builder
for j := 0; j < 100; j++ {
sb.WriteString("x")
}
_ = sb.String()
}
}
11. Quizzes
Quiz 1: assert vs require
What is the difference between assert.Equal and require.Equal? When should you use each?
Answer:
assert.Equal: test continues running even on failure. Callst.Error()internally.require.Equal: test stops immediately on failure. Callst.FailNow()internally.
Explanation:
Use require when subsequent code depends on a precondition:
func TestUserCreation(t *testing.T) {
user, err := CreateUser("Alice", "alice@example.com")
require.NoError(t, err) // if error, user is nil, next line would panic
require.NotNil(t, user)
assert.Equal(t, "Alice", user.Name) // independent assertions use assert
assert.NotEmpty(t, user.ID)
}
Use assert when you want to check multiple independent conditions and see all failures at once.
Quiz 2: Table-Driven Test Closure Trap
What is the bug in the following table-driven test?
tests := []struct {
name string
input int
want int
}{
{"double 2", 2, 4},
{"double 5", 5, 10},
{"double 10", 10, 20},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := Double(tt.input)
assert.Equal(t, tt.want, result)
})
}
Answer: With t.Parallel(), the closure shares the loop variable tt.
Explanation: When t.Parallel() is called, the subtest runs concurrently. The closure captures tt by reference, so all goroutines share the same variable. By the time they run, the loop has finished and tt holds the last value.
Fix:
for _, tt := range tests {
tt := tt // capture loop variable in local variable
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
result := Double(tt.input)
assert.Equal(t, tt.want, result)
})
}
In Go 1.22+, loop variable semantics changed and this issue no longer applies.
Quiz 3: Mock vs Stub
Explain the difference between a Mock and a Stub, and when to use each.
Answer:
- Stub: a simple replacement that returns pre-configured responses. Used for state verification.
- Mock: an object that verifies specific methods were called with specific arguments. Used for behavior verification.
Explanation:
Stub example — fixed response:
type StubUserRepo struct{}
func (s StubUserRepo) FindByID(ctx context.Context, id int) (*User, error) {
return &User{ID: id, Name: "Test User"}, nil
}
Mock example — call verification:
mockRepo.EXPECT().FindByID(ctx, 1).Return(user, nil).Times(1)
// After the test, automatically verifies FindByID was called exactly once
Rule of thumb: use a Mock when your test needs to verify whether/how many times/with what arguments a method was called. Use a Stub when you simply need a certain response to be returned.
Quiz 4: The go test -race Flag
What does go test -race detect, and when should you use it?
Answer: It detects race conditions — situations where multiple goroutines access the same memory location concurrently without synchronization.
Explanation:
// This code is caught by -race
var counter int
func TestRaceCondition(t *testing.T) {
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter++ // DATA RACE: unsynchronized write
}()
}
wg.Wait()
}
The -race flag has significant runtime overhead (5–10x slower), so it's best used in CI/CD pipelines or periodically when writing concurrent code rather than on every local test run.
Quiz 5: The 100% Coverage Trap
Does 100% test coverage guarantee that code is bug-free? What are the pitfalls of measuring coverage?
Answer: No. 100% coverage does not guarantee the absence of bugs.
Explanation: Coverage measures whether code was executed, not whether it behaves correctly.
func Divide(a, b int) int {
return a / b
}
// 100% line coverage but misses a critical case
func TestDivide(t *testing.T) {
assert.Equal(t, 5, Divide(10, 2))
// No test for b=0, which causes a panic at runtime
}
More important than coverage numbers:
- Boundary value testing (0, -1, max values, etc.)
- Error path testing
- Concurrency testing
- Integration testing
Coverage is a tool to find untested code paths, not a measure of test quality. A test suite with 70% meaningful coverage is far more valuable than one with 100% trivial coverage.