Go Testing with Testify: Production Unit Test Patterns and Best Practices 2026
When if err != nil Meets reflect.DeepEqual: The Go Testing Pain Loop
Have you written tests like this?
func TestAdd(t *testing.T) {
result := Add(1, 2)
if result != 3 {
t.Errorf("Add(1, 2) = %d; want 3", result)
}
}
A simple addition test looks fine. But when you need to test slice equality, map comparison, deep struct comparison, or error type matching, the code becomes a nightmare — reflect.DeepEqual everywhere, error messages with zero readability, and when tests fail you only know "not equal" without knowing what's different. Not to mention Go's standard library has no built-in mock framework, interface simulation is all manual, and test organization relies on if statements piling up.
This isn't an isolated case. At Google, Uber, and other major companies running Go projects, test code without testify is often longer than the business code itself. This article covers 5 production-grade testing patterns to help you master the testify framework, evolving your Go tests from "just runs" to "professional and reliable."
Core Concepts Reference
| Component | Purpose | Key Features | Typical Use Case |
|---|---|---|---|
assert |
Assert and record failures | Continues after failure, collects all errors | Unit tests with multiple assertions |
require |
Assert and stop immediately | Stops current test on failure, avoids nil pointer cascade | Precondition checks |
suite |
Test suite organization | Setup/TearDown lifecycle, shared state | Integration tests, database tests |
mock |
Interface simulation | Auto-generated method call assertions | Dependency decoupling, external service simulation |
httptest |
HTTP test server | Real HTTP request simulation without starting a server | API Handler testing |
5 Challenges of Go Testing
Challenge 1: Verbose Assertions with Unfriendly Messages
func TestUserEqual(t *testing.T) {
got := GetUser(1)
want := User{ID: 1, Name: "Alice", Age: 30}
if got.ID != want.ID {
t.Errorf("ID: got %d, want %d", got.ID, want.ID)
}
if got.Name != want.Name {
t.Errorf("Name: got %s, want %s", got.Name, want.Name)
}
if got.Age != want.Age {
t.Errorf("Age: got %d, want %d", got.Age, want.Age)
}
}
Three fields require three if blocks. What about ten fields? Nested structs? reflect.DeepEqual can compare, but the failure message only says not equal — you have no idea which field differs.
Challenge 2: No Built-in Mock, Hand-written Simulation Code Explodes
type UserRepository interface {
GetByID(ctx context.Context, id int64) (*User, error)
Create(ctx context.Context, user *User) error
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context, filter Filter) ([]*User, error)
}
An interface with 5 methods requires implementing 5 methods for a hand-written mock, even if a test only uses 1. Ten interfaces means 50 method implementations — mock code exceeds business code.
Challenge 3: Disorganized Tests, Repeated Setup/TearDown
Every test needs db.Connect(), every test end needs db.Close(), plus test data cleanup. Copy-paste everywhere, and missing TearDown pollutes the next test.
Challenge 4: Table-Driven Test Assertion Dilemma
Table-driven tests are Go's idiomatic pattern, but standard library assertions perform poorly — t.Errorf doesn't tell you which specific test case failed; you can only distinguish by manually adding case names.
Challenge 5: HTTP Tests Depend on Real Services
Testing API handlers — do you start an HTTP server? Use httptest.NewRecorder? How to construct request bodies? How to assert responses? No unified pattern means ten team members write ten different styles.
Pattern 1: assert vs require — Two Philosophies of Assertion
assert vs require Core Differences
package user_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAssertVsRequire(t *testing.T) {
// assert: continues after failure, collects all errors
assert.Equal(t, 1, 2, "first assertion")
assert.Equal(t, 3, 3, "second assertion") // still executes
assert.NotNil(t, nil, "third assertion") // still executes
// Test result: 3 failures, you see all problems at once
}
func TestRequireBehavior(t *testing.T) {
// require: stops current test immediately on failure
require.Equal(t, 1, 2, "precondition not met")
// This line never executes
require.Equal(t, 3, 3, "won't reach here")
}
Production-Grade Assertion Practice
package service_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"myapp/model"
"myapp/service"
)
func TestCreateUser(t *testing.T) {
// require for preconditions: creation must succeed, otherwise subsequent assertions are meaningless
user, err := service.CreateUser("alice", "alice@example.com")
require.NoError(t, err, "creating user should not return an error")
require.NotNil(t, user, "creating user should not return nil")
// assert for business assertions: even if one field is wrong, you want to see other field results
assert.Equal(t, "alice", user.Name, "username should match")
assert.Equal(t, "alice@example.com", user.Email, "email should match")
assert.NotZero(t, user.ID, "user ID should not be zero")
assert.WithinDuration(t, time.Now(), user.CreatedAt, time.Second, "creation time should be close to now")
}
Common Assertion Methods Reference
// Equality
assert.Equal(t, expected, actual)
assert.NotEqual(t, expected, actual)
assert.Exactly(t, expected, actual) // type + value exactly the same
// Boolean
assert.True(t, condition)
assert.False(t, condition)
// Nil checks
assert.Nil(t, obj)
assert.NotNil(t, obj)
// Error checks
assert.NoError(t, err)
assert.Error(t, err)
assert.ErrorIs(t, err, os.ErrNotExist)
assert.ErrorContains(t, err, "not found")
// Collections
assert.Contains(t, slice, element)
assert.NotContains(t, slice, element)
assert.ElementsMatch(t, []int{1,2,3}, []int{3,1,2}) // unordered match
assert.Subset(t, []int{1,2,3,4}, []int{2,3})
// Type checks
assert.IsType(t, &User{}, actual)
assert.Implements(t, (*Repository)(nil), actual)
// Numeric comparisons
assert.Greater(t, 10, 5)
assert.LessOrEqual(t, 5, 5)
assert.InDelta(t, 3.14, 3.14159, 0.01)
// Strings
assert.Empty(t, "")
assert.Regexp(t, `^\d+$`, "12345")
Pattern 2: Table-Driven Tests + testify suite
Basic Table-Driven Test
package calculator_test
import (
"testing"
"github.com/stretchr/testify/assert"
"myapp/calculator"
)
func TestDivide(t *testing.T) {
tests := []struct {
name string
a float64
b float64
want float64
wantErr bool
errContains string
}{
{
name: "normal division",
a: 10.0,
b: 2.0,
want: 5.0,
wantErr: false,
},
{
name: "divide by zero",
a: 10.0,
b: 0.0,
wantErr: true,
errContains: "division by zero",
},
{
name: "negative division",
a: -10.0,
b: 2.0,
want: -5.0,
wantErr: false,
},
{
name: "float precision",
a: 1.0,
b: 3.0,
want: 0.333333,
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := calculator.Divide(tt.a, tt.b)
if tt.wantErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), tt.errContains)
return
}
assert.NoError(t, err)
assert.InDelta(t, tt.want, got, 0.001)
})
}
}
testify suite: Test Suite with Lifecycle
package repository_test
import (
"context"
"testing"
"github.com/stretchr/testify/suite"
"myapp/model"
"myapp/repository"
"myapp/testutil"
)
type UserRepositorySuite struct {
suite.Suite // Embed suite.Suite to get assert/require methods
db *testutil.TestDB
repo repository.UserRepository
ctx context.Context
}
// SetupSuite runs once before all tests
func (s *UserRepositorySuite) SetupSuite() {
s.db = testutil.NewTestDB()
s.db.Migrate()
s.ctx = context.Background()
}
// TearDownSuite runs once after all tests
func (s *UserRepositorySuite) TearDownSuite() {
s.db.Close()
}
// SetupTest runs before each test
func (s *UserRepositorySuite) SetupTest() {
s.db.TruncateTables()
s.repo = repository.NewUserRepository(s.db.Conn())
}
// TearDownTest runs after each test
func (s *UserRepositorySuite) TearDownTest() {
// Clean up caches etc.
}
func (s *UserRepositorySuite) TestCreate() {
user := &model.User{
Name: "Alice",
Email: "alice@example.com",
}
err := s.repo.Create(s.ctx, user)
s.NoError(err)
s.NotZero(user.ID)
found, err := s.repo.GetByID(s.ctx, user.ID)
s.NoError(err)
s.Equal("Alice", found.Name)
s.Equal("alice@example.com", found.Email)
}
func (s *UserRepositorySuite) TestGetByIDNotFound() {
_, err := s.repo.GetByID(s.ctx, 99999)
s.Error(err)
s.ErrorIs(err, repository.ErrNotFound)
}
func (s *UserRepositorySuite) TestList() {
users := []*model.User{
{Name: "Alice", Email: "alice@example.com"},
{Name: "Bob", Email: "bob@example.com"},
{Name: "Charlie", Email: "charlie@example.com"},
}
for _, u := range users {
s.NoError(s.repo.Create(s.ctx, u))
}
result, err := s.repo.List(s.ctx, repository.Filter{Limit: 10})
s.NoError(err)
s.Len(result, 3)
}
// Entry function: must call suite.Run
func TestUserRepositorySuite(t *testing.T) {
suite.Run(t, new(UserRepositorySuite))
}
Pattern 3: Mock and Interface Simulation
testify/mock Basic Usage
package service_test
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"myapp/model"
"myapp/service"
)
// MockUserRepository simulates the UserRepository interface
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) GetByID(ctx context.Context, id int64) (*model.User, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*model.User), args.Error(1)
}
func (m *MockUserRepository) Create(ctx context.Context, user *model.User) error {
args := m.Called(ctx, user)
return args.Error(0)
}
func (m *MockUserRepository) Update(ctx context.Context, user *model.User) error {
args := m.Called(ctx, user)
return args.Error(0)
}
func (m *MockUserRepository) Delete(ctx context.Context, id int64) error {
args := m.Called(ctx, id)
return args.Error(0)
}
func (m *MockUserRepository) List(ctx context.Context, filter repository.Filter) ([]*model.User, error) {
args := m.Called(ctx, filter)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*model.User), args.Error(1)
}
Service Layer Testing: Verify Call Behavior
func TestUserService_GetUser(t *testing.T) {
mockRepo := new(MockUserRepository)
svc := service.NewUserService(mockRepo)
t.Run("user exists", func(t *testing.T) {
expectedUser := &model.User{ID: 1, Name: "Alice", Email: "alice@example.com"}
mockRepo.On("GetByID", mock.Anything, int64(1)).
Return(expectedUser, nil).Once()
user, err := svc.GetUser(context.Background(), 1)
require.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
mockRepo.AssertExpectations(t)
})
t.Run("user not found", func(t *testing.T) {
mockRepo.On("GetByID", mock.Anything, int64(999)).
Return(nil, repository.ErrNotFound).Once()
_, err := svc.GetUser(context.Background(), 999)
assert.ErrorIs(t, err, repository.ErrNotFound)
mockRepo.AssertExpectations(t)
})
}
func TestUserService_CreateUser_DuplicateEmail(t *testing.T) {
mockRepo := new(MockUserRepository)
svc := service.NewUserService(mockRepo)
mockRepo.On("Create", mock.Anything, mock.MatchedBy(func(u *model.User) bool {
return u.Email == "duplicate@example.com"
})).Return(repository.ErrDuplicateEmail).Once()
err := svc.CreateUser(context.Background(), "Alice", "duplicate@example.com")
assert.ErrorIs(t, err, repository.ErrDuplicateEmail)
mockRepo.AssertExpectations(t)
}
Advanced Mock: Matchers and Call Counts
func TestUserService_ComplexMock(t *testing.T) {
mockRepo := new(MockUserRepository)
svc := service.NewUserService(mockRepo)
mockRepo.On("Create",
mock.Anything,
mock.MatchedBy(func(u *model.User) bool {
return u.Name != "" && u.Email != ""
}),
).Return(nil).Once()
mockRepo.On("Delete", mock.Anything, mock.Anything).Return(nil).Times(0)
err := svc.CreateUser(context.Background(), "Alice", "alice@example.com")
require.NoError(t, err)
mockRepo.AssertExpectations(t)
mockRepo.AssertCalled(t, "Create", mock.Anything, mock.Anything)
mockRepo.AssertNumberOfCalls(t, "Create", 1)
mockRepo.AssertNotCalled(t, "Delete", mock.Anything, mock.Anything)
}
Pattern 4: HTTP Handler Testing
Testing Gin Handlers with httptest
package handler_test
import (
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"myapp/handler"
"myapp/model"
)
func init() {
gin.SetMode(gin.TestMode)
}
func TestGetUserHandler(t *testing.T) {
mockSvc := new(MockUserService)
h := handler.NewUserHandler(mockSvc)
router := gin.New()
router.GET("/api/v1/users/:id", h.GetUser)
t.Run("successfully get user", func(t *testing.T) {
expectedUser := &model.User{ID: 1, Name: "Alice", Email: "alice@example.com"}
mockSvc.On("GetUser", mock.Anything, int64(1)).Return(expectedUser, nil).Once()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/users/1", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var resp struct {
Code int `json:"code"`
Data *model.User `json:"data"`
}
err := json.Unmarshal(w.Body.Bytes(), &resp)
require.NoError(t, err)
assert.Equal(t, 0, resp.Code)
assert.Equal(t, "Alice", resp.Data.Name)
mockSvc.AssertExpectations(t)
})
t.Run("user not found returns 404", func(t *testing.T) {
mockSvc.On("GetUser", mock.Anything, int64(999)).
Return(nil, service.ErrUserNotFound).Once()
w := httptest.NewRecorder()
req, _ := http.NewRequest("GET", "/api/v1/users/999", nil)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusNotFound, w.Code)
mockSvc.AssertExpectations(t)
})
}
func TestCreateUserHandler(t *testing.T) {
mockSvc := new(MockUserService)
h := handler.NewUserHandler(mockSvc)
router := gin.New()
router.POST("/api/v1/users", h.CreateUser)
t.Run("create successfully", func(t *testing.T) {
mockSvc.On("CreateUser", mock.Anything, "Alice", "alice@example.com").
Return(&model.User{ID: 1, Name: "Alice", Email: "alice@example.com"}, nil).Once()
body := `{"name":"Alice","email":"alice@example.com"}`
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
mockSvc.AssertExpectations(t)
})
t.Run("validation failure", func(t *testing.T) {
body := `{"name":"","email":"invalid"}`
w := httptest.NewRecorder()
req, _ := http.NewRequest("POST", "/api/v1/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusBadRequest, w.Code)
})
}
httptest.NewServer: Simulating External HTTP Services
package client_test
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"myapp/client"
)
func TestWeatherClient_GetWeather(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/weather", r.URL.Path)
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
assert.Equal(t, "Beijing", r.URL.Query().Get("city"))
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(`{"city":"Beijing","temp":25,"humidity":60}`))
}))
defer server.Close()
c := client.NewWeatherClient(server.URL, "test-token")
weather, err := c.GetWeather(context.Background(), "Beijing")
require.NoError(t, err)
assert.Equal(t, "Beijing", weather.City)
assert.Equal(t, 25, weather.Temp)
assert.Equal(t, 60, weather.Humidity)
}
Pattern 5: Test Suite Organization and CI Integration
Project Test Directory Structure
myapp/
├── internal/
│ ├── user/
│ │ ├── service.go
│ │ ├── service_test.go
│ │ ├── handler.go
│ │ ├── handler_test.go
│ │ ├── mock_repository.go
│ │ └── testdata/
│ │ ├── create_user_req.json
│ │ └── get_user_resp.json
│ └── order/
│ ├── service.go
│ ├── service_test.go
│ └── suite_test.go
├── integration/
│ └── user_repository_test.go
├── testutil/
│ ├── db.go
│ └── fixture.go
├── Makefile
└── .github/
└── workflows/
└── test.yml
Makefile Test Commands
.PHONY: test test-unit test-integration test-coverage test-race
test-unit:
go test -v -count=1 -short ./internal/...
test-integration:
go test -v -count=1 -tags=integration ./integration/...
test-race:
go test -v -race -count=1 ./...
test-coverage:
go test -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -html=coverage.out -o coverage.html
@echo "Coverage: $$(go tool cover -func=coverage.out | grep total | awk '{print $$3}')"
test: test-unit test-coverage
GitHub Actions CI Configuration
name: Test
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
go-version: ['1.24', '1.25']
steps:
- uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
- name: Cache Go modules
uses: actions/cache@v4
with:
path: |
~/go/pkg/mod
~/.cache/go-build
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}
- name: Download dependencies
run: go mod download
- name: Vet
run: go vet ./...
- name: Test with race detector
run: go test -race -count=1 -short ./...
- name: Generate coverage
run: |
go test -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -func=coverage.out
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage.out
Test Suite Integration Example
package integration_test
import (
"context"
"testing"
"github.com/stretchr/testify/suite"
"myapp/model"
"myapp/repository"
"myapp/testutil"
)
type IntegrationSuite struct {
suite.Suite
db *testutil.TestDB
repos *repository.Repositories
}
func (s *IntegrationSuite) SetupSuite() {
s.db = testutil.NewTestDB()
s.db.Migrate()
s.repos = repository.NewRepositories(s.db.Conn())
}
func (s *IntegrationSuite) TearDownSuite() {
s.db.Close()
}
func (s *IntegrationSuite) SetupTest() {
s.db.TruncateTables()
}
func (s *IntegrationSuite) TestUserCRUD() {
ctx := context.Background()
user := &model.User{Name: "Alice", Email: "alice@example.com"}
s.NoError(s.repos.User.Create(ctx, user))
s.NotZero(user.ID)
found, err := s.repos.User.GetByID(ctx, user.ID)
s.NoError(err)
s.Equal("Alice", found.Name)
found.Name = "Bob"
s.NoError(s.repos.User.Update(ctx, found))
updated, _ := s.repos.User.GetByID(ctx, user.ID)
s.Equal("Bob", updated.Name)
s.NoError(s.repos.User.Delete(ctx, user.ID))
_, err = s.repos.User.GetByID(ctx, user.ID)
s.Error(err)
}
func TestIntegrationSuite(t *testing.T) {
suite.Run(t, new(IntegrationSuite))
}
5 Common Pitfalls
Pitfall 1: Using require in Table-Driven Tests Skips Subsequent Assertions
❌ Wrong:
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := DoSomething(tt.input)
require.NoError(t, err) // Skips subsequent assertions on failure
require.Equal(t, tt.want, result)
})
}
✅ Correct:
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := DoSomething(tt.input)
assert.NoError(t, err)
assert.Equal(t, tt.want, result)
})
}
Note:
t.Runcreates independent subtests.requireonly terminates the subtest it's in, not other cases. But within a single subtest, if you want to see all assertion results, useassert.
Pitfall 2: Forgetting to Call AssertExpectations
❌ Wrong:
func TestWithMock(t *testing.T) {
m := new(MockRepo)
m.On("Get", mock.Anything, 1).Return(nil, nil)
// No AssertExpectations!
}
✅ Correct:
func TestWithMock(t *testing.T) {
m := new(MockRepo)
m.On("Get", mock.Anything, 1).Return(nil, nil).Once()
// ... use m
m.AssertExpectations(t)
}
Pitfall 3: Mock Return Type Mismatch Causes Panic
❌ Wrong:
m.On("GetByID", mock.Anything, int64(1)).Return(nil, nil)
// nil type ambiguity, may cause panic
✅ Correct:
m.On("GetByID", mock.Anything, int64(1)).Return(&model.User{ID: 1}, nil)
// Explicitly return concrete type values
Pitfall 4: Using t.Run Directly Inside suite
❌ Wrong:
func (s *MySuite) TestSomething() {
s.T().Run("subtest", func(t *testing.T) {
s.Equal(1, 2) // s.T() may point to wrong t
})
}
✅ Correct:
func (s *MySuite) TestSomething() {
s.Equal(1, 1)
s.NoError(nil)
}
// Or split into separate test methods
func (s *MySuite) TestSomethingCase1() {
s.Equal(1, 1)
}
func (s *MySuite) TestSomethingCase2() {
s.Equal(2, 2)
}
Pitfall 5: Forgetting Content-Type in httptest
❌ Wrong:
req, _ := http.NewRequest("POST", "/api/users", strings.NewReader(body))
// No Content-Type set
router.ServeHTTP(w, req)
✅ Correct:
req, _ := http.NewRequest("POST", "/api/users", strings.NewReader(body))
req.Header.Set("Content-Type", "application/json")
router.ServeHTTP(w, req)
Error Troubleshooting Reference
| Error Message | Cause | Solution |
|---|---|---|
assert: arguments: Call may need to use WithArgs |
Mock On method parameter types don't match actual call | Ensure On parameter types match actual calls, use mock.Anything for any parameter |
mock: Unexpected Method Call |
Called a Mock method not registered via On | Check if On registration was missed, or add .Maybe() for optional calls |
panic: interface conversion: interface {} is nil, not *X |
Mock Return nil cannot convert to concrete type | Use Return(&X{}, nil) instead of Return(nil, nil) |
suite: test method should have signature TestXxx(t *testing.T) |
Suite entry function signature error | Ensure entry function is func TestXxxSuite(t *testing.T) and calls suite.Run |
cannot use m (type *MockRepo) as type Repo in assignment |
Mock struct doesn't fully implement interface | Use mockgen tool to auto-generate Mock, ensure all interface methods are implemented |
test timed out after 10m0s |
Mock method blocking, waiting but never called | Check if business logic correctly calls Mock method, or add .Maybe() |
race detected during execution of test |
Concurrent data race in test | Run tests with -race flag, check shared state access |
testify: AssertExpectations failed |
Registered Mock method was never called | Check if business logic missed method call, or use .Maybe() for optional calls |
invalid argument for Int method |
Mock parameter used wrong type | Use int64(1) instead of 1, ensure parameter types match interface signature |
go: module github.com/stretchr/testify not found |
testify dependency not installed | Run go get github.com/stretchr/testify@v1.9.0 |
Advanced Optimization Tips
Tip 1: Custom Assertions to Reduce Repetitive Code
package testutil
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"myapp/model"
)
// AssertUserEqual custom user assertion, ignoring ID and CreatedAt
func AssertUserEqual(t *testing.T, expected, actual *model.User, msgAndArgs ...interface{}) {
t.Helper()
assert.Equal(t, expected.Name, actual.Name, msgAndArgs...)
assert.Equal(t, expected.Email, actual.Email, msgAndArgs...)
assert.Equal(t, expected.Age, actual.Age, msgAndArgs...)
if expected.Status != "" {
assert.Equal(t, expected.Status, actual.Status, msgAndArgs...)
}
}
// AssertTimeApproximately time proximity assertion
func AssertTimeApproximately(t *testing.T, expected, actual time.Time, delta time.Duration, msgAndArgs ...interface{}) {
t.Helper()
assert.WithinDuration(t, expected, actual, delta, msgAndArgs...)
}
Tip 2: Auto-generate Mocks with mockgen
# Install mockgen
go install go.uber.org/mock/mockgen@latest
# Generate Mock for interface
mockgen -source=repository.go -destination=mock_repository.go -package=repository_test
# Use comment directive for auto-generation
//go:generate mockgen -source=repository.go -destination=mock_repository.go -package=repository_test
Tip 3: Test Subset Execution with Build Tags
// Use build tag to control integration tests
//go:build integration
package integration_test
import "testing"
func TestDatabaseCRUD(t *testing.T) {
// Only runs with go test -tags=integration
}
# Run unit tests only
go test -short ./...
# Run integration tests
go test -tags=integration ./...
Tip 4: Parallel Test Acceleration
func TestParallel(t *testing.T) {
t.Parallel()
tests := []struct{ name string }{
{name: "case1"},
{name: "case2"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// Test logic
})
}
}
Tip 5: Golden File Testing Pattern
package handler_test
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetUserResponse(t *testing.T) {
handler := setupHandler()
resp := handler.GetUser(context.Background(), 1)
actual, err := json.MarshalIndent(resp, "", " ")
require.NoError(t, err)
goldenPath := filepath.Join("testdata", "get_user_response.json")
if os.Getenv("UPDATE_GOLDEN") != "" {
err = os.WriteFile(goldenPath, actual, 0644)
require.NoError(t, err)
}
expected, err := os.ReadFile(goldenPath)
require.NoError(t, err)
assert.Equal(t, string(expected), string(actual))
}
# Update golden files
UPDATE_GOLDEN=1 go test ./handler/...
Testing Framework Comparison
| Feature | testify | gomock | gocheck | Standard testing |
|---|---|---|---|---|
| Assertion Richness | ⭐⭐⭐⭐⭐ 100+ methods | ⭐⭐ Basic assertions | ⭐⭐⭐ Fairly rich | ⭐ Only t.Error |
| Mock Support | ⭐⭐⭐⭐ Built-in mock package | ⭐⭐⭐⭐⭐ mockgen auto-generation | ⭐⭐ No built-in | ❌ None |
| Test Suite | ⭐⭐⭐⭐ suite package | ❌ None | ⭐⭐⭐⭐⭐ Checker+Fixture | ⭐ Only subtests |
| Learning Curve | ⭐ Low, intuitive API | ⭐⭐⭐ Medium, requires mockgen | ⭐⭐⭐ Medium | ⭐ Lowest |
| Community Activity | ⭐⭐⭐⭐⭐ GitHub 23k+ stars | ⭐⭐⭐⭐ Uber maintained | ⭐⭐ Niche | ⭐⭐⭐⭐⭐ Official |
| Table-Driven Compat | ⭐⭐⭐⭐⭐ Perfect | ⭐⭐⭐ Needs adaptation | ⭐⭐⭐ Needs adaptation | ⭐⭐⭐⭐ Native support |
| HTTP Testing | ⭐⭐⭐⭐ With httptest | ⭐⭐⭐ With httptest | ⭐⭐⭐ With httptest | ⭐⭐⭐ With httptest |
| IDE Support | ⭐⭐⭐⭐⭐ All IDEs | ⭐⭐⭐⭐ All IDEs | ⭐⭐ Partial | ⭐⭐⭐⭐⭐ All IDEs |
| Use Case | General testing, quick start | Interface mock-heavy projects | Complex fixture projects | Simple projects, minimalism |
Summary
The core problem of Go testing isn't "how to write assertions" but "how to organize tests." assert and require solve assertion readability, suite solves test lifecycle management, mock solves dependency isolation, httptest solves HTTP test reliability, and CI integration solves test continuity. All five combined form the complete methodology for production-grade Go testing. Remember: a good test isn't one that's written extensively, but one that precisely pinpoints problems when it fails.
Related Tools
- JSON Formatter — Format API response JSON, quickly verify data structures in test assertions
- Hash Calculator — Calculate request signatures and data checksums, ensure mock data consistency
- cURL to Code — Convert cURL commands to Go HTTP test code, accelerate handler test writing
Try these browser-local tools — no sign-up required →