golang-testing-strategies
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGo Testing Strategies
Go测试策略
Overview
概述
Go provides a robust built-in testing framework ( package) that emphasizes simplicity and developer productivity. Combined with community tools like testify and gomock, Go testing enables comprehensive test coverage with minimal boilerplate.
testingKey Features:
- 📋 Table-Driven Tests: Idiomatic pattern for testing multiple inputs
- ✅ Testify: Readable assertions and test suites
- 🎭 Gomock: Type-safe interface mocking
- ⚡ Benchmarking: Built-in performance testing
- 🔍 Race Detector: Concurrent code safety verification
- 📊 Coverage: Native coverage reporting and enforcement
- 🚀 CI Integration: Test caching and parallel execution
Go提供了一个强大的内置测试框架(包),强调简洁性和开发者生产力。结合Testify、Gomock等社区工具,Go测试能够以极少的样板代码实现全面的测试覆盖。
testing核心特性:
- 📋 表驱动测试: 用于测试多组输入的惯用模式
- ✅ Testify: 可读性强的断言和测试套件
- 🎭 Gomock: 类型安全的接口模拟
- ⚡ 基准测试: 内置的性能测试
- 🔍 竞态检测器: 并发代码安全验证
- 📊 覆盖率: 原生的覆盖率报告与强制检查
- 🚀 CI集成: 测试缓存与并行执行
When to Use This Skill
何时使用该技能
Activate this skill when:
- Writing test suites for Go libraries or applications
- Setting up testing infrastructure for new projects
- Mocking external dependencies (databases, APIs, services)
- Benchmarking performance-critical code paths
- Ensuring thread-safe concurrent implementations
- Integrating tests into CI/CD pipelines
- Migrating from other testing frameworks
在以下场景中启用此技能:
- 为Go库或应用编写测试套件
- 为新项目搭建测试基础设施
- 模拟外部依赖(数据库、API、服务)
- 对性能关键代码路径进行基准测试
- 确保线程安全的并发实现
- 将测试集成到CI/CD流水线
- 从其他测试框架迁移
Core Testing Principles
核心测试原则
The Go Testing Philosophy
Go测试理念
- Simplicity Over Magic: Use standard library when possible
- Table-Driven Tests: Test multiple scenarios with single function
- Subtests: Organize related tests with
t.Run() - Interface-Based Mocking: Mock dependencies through interfaces
- Test Files Colocate: Place files alongside code
*_test.go - Package Naming: Use for external tests,
package_testfor internalpackage
- 简洁优先,避免魔法: 尽可能使用标准库
- 表驱动测试: 用单个函数测试多个场景
- 子测试: 使用组织相关测试
t.Run() - 基于接口的模拟: 通过接口模拟依赖
- 测试文件同目录放置: 将文件与代码放在一起
*_test.go - 包命名: 外部测试使用,内部测试使用原包名
package_test
Test Organization
测试组织
File Naming Convention:
- Unit tests:
file_test.go - Integration tests:
file_integration_test.go - Benchmark tests: Prefix with in same test file
Benchmark
Package Structure:
mypackage/
├── user.go
├── user_test.go // Internal tests (same package)
├── user_external_test.go // External tests (package mypackage_test)
├── integration_test.go // Integration tests
└── testdata/ // Test fixtures (ignored by go build)
└── golden.json文件命名规范:
- 单元测试:
file_test.go - 集成测试:
file_integration_test.go - 基准测试: 在同一测试文件中以为前缀
Benchmark
包结构:
mypackage/
├── user.go
├── user_test.go // 内部测试(同包)
├── user_external_test.go // 外部测试(包名为mypackage_test)
├── integration_test.go // 集成测试
└── testdata/ // 测试夹具(go build会忽略)
└── golden.jsonTable-Driven Test Pattern
表驱动测试模式
Basic Structure
基本结构
The idiomatic Go testing pattern for testing multiple inputs:
go
func TestUserValidation(t *testing.T) {
tests := []struct {
name string
input User
wantErr bool
errMsg string
}{
{
name: "valid user",
input: User{Name: "Alice", Age: 30, Email: "alice@example.com"},
wantErr: false,
},
{
name: "empty name",
input: User{Name: "", Age: 30, Email: "alice@example.com"},
wantErr: true,
errMsg: "name is required",
},
{
name: "invalid email",
input: User{Name: "Bob", Age: 25, Email: "invalid"},
wantErr: true,
errMsg: "invalid email format",
},
{
name: "negative age",
input: User{Name: "Charlie", Age: -5, Email: "charlie@example.com"},
wantErr: true,
errMsg: "age must be positive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateUser(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr && err.Error() != tt.errMsg {
t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
}
})
}
}Go中用于测试多组输入的惯用模式:
go
func TestUserValidation(t *testing.T) {
tests := []struct {
name string
input User
wantErr bool
errMsg string
}{
{
name: "valid user",
input: User{Name: "Alice", Age: 30, Email: "alice@example.com"},
wantErr: false,
},
{
name: "empty name",
input: User{Name: "", Age: 30, Email: "alice@example.com"},
wantErr: true,
errMsg: "name is required",
},
{
name: "invalid email",
input: User{Name: "Bob", Age: 25, Email: "invalid"},
wantErr: true,
errMsg: "invalid email format",
},
{
name: "negative age",
input: User{Name: "Charlie", Age: -5, Email: "charlie@example.com"},
wantErr: true,
errMsg: "age must be positive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateUser(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateUser() error = %v, wantErr %v", err, tt.wantErr)
return
}
if tt.wantErr && err.Error() != tt.errMsg {
t.Errorf("ValidateUser() error message = %v, want %v", err.Error(), tt.errMsg)
}
})
}
}Parallel Test Execution
并行测试执行
Enable parallel test execution for independent tests:
go
func TestConcurrentOperations(t *testing.T) {
tests := []struct {
name string
fn func() int
want int
}{
{"operation 1", func() int { return compute1() }, 42},
{"operation 2", func() int { return compute2() }, 84},
{"operation 3", func() int { return compute3() }, 126},
}
for _, tt := range tests {
tt := tt // Capture range variable
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // Run tests concurrently
got := tt.fn()
if got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}为独立测试启用并行执行:
go
func TestConcurrentOperations(t *testing.T) {
tests := []struct {
name string
fn func() int
want int
}{
{"operation 1", func() int { return compute1() }, 42},
{"operation 2", func() int { return compute2() }, 84},
{"operation 3", func() int { return compute3() }, 126},
}
for _, tt := range tests {
tt := tt // 捕获循环变量
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // 并发运行测试
got := tt.fn()
if got != tt.want {
t.Errorf("got %v, want %v", got, tt.want)
}
})
}
}Testify Framework
Testify框架
Installation
安装
bash
go get github.com/stretchr/testifybash
go get github.com/stretchr/testifyAssertions
断言
Replace verbose error checking with readable assertions:
go
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCalculator(t *testing.T) {
calc := NewCalculator()
// assert: Test continues on failure
assert.Equal(t, 5, calc.Add(2, 3))
assert.NotNil(t, calc)
assert.True(t, calc.IsReady())
// require: Test stops on failure (for critical assertions)
result, err := calc.Divide(10, 2)
require.NoError(t, err) // Stop if error occurs
assert.Equal(t, 5, result)
}
func TestUserOperations(t *testing.T) {
user := &User{ID: 1, Name: "Alice", Email: "alice@example.com"}
// Object matching
assert.Equal(t, 1, user.ID)
assert.Contains(t, user.Email, "@")
assert.Len(t, user.Name, 5)
// Partial matching
assert.ObjectsAreEqual(user, &User{
ID: 1,
Name: "Alice",
Email: assert.AnythingOfType("string"),
})
}用可读性强的断言替代冗长的错误检查:
go
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestCalculator(t *testing.T) {
calc := NewCalculator()
// assert: 测试失败后继续执行
assert.Equal(t, 5, calc.Add(2, 3))
assert.NotNil(t, calc)
assert.True(t, calc.IsReady())
// require: 测试失败后立即停止(用于关键断言)
result, err := calc.Divide(10, 2)
require.NoError(t, err) // 若出现错误则停止
assert.Equal(t, 5, result)
}
func TestUserOperations(t *testing.T) {
user := &User{ID: 1, Name: "Alice", Email: "alice@example.com"}
// 对象匹配
assert.Equal(t, 1, user.ID)
assert.Contains(t, user.Email, "@")
assert.Len(t, user.Name, 5)
// 部分匹配
assert.ObjectsAreEqual(user, &User{
ID: 1,
Name: "Alice",
Email: assert.AnythingOfType("string"),
})
}Test Suites
测试套件
Organize related tests with setup/teardown:
go
import (
"testing"
"github.com/stretchr/testify/suite"
)
type UserServiceTestSuite struct {
suite.Suite
db *sql.DB
service *UserService
}
// SetupSuite runs once before all tests
func (s *UserServiceTestSuite) SetupSuite() {
s.db = setupTestDatabase()
s.service = NewUserService(s.db)
}
// TearDownSuite runs once after all tests
func (s *UserServiceTestSuite) TearDownSuite() {
s.db.Close()
}
// SetupTest runs before each test
func (s *UserServiceTestSuite) SetupTest() {
cleanDatabase(s.db)
}
// TearDownTest runs after each test
func (s *UserServiceTestSuite) TearDownTest() {
// Cleanup if needed
}
// Test methods must start with "Test"
func (s *UserServiceTestSuite) TestCreateUser() {
user := &User{Name: "Alice", Email: "alice@example.com"}
err := s.service.Create(user)
s.NoError(err)
s.NotEqual(0, user.ID) // ID assigned
}
func (s *UserServiceTestSuite) TestGetUser() {
// Setup
user := &User{Name: "Bob", Email: "bob@example.com"}
s.service.Create(user)
// Test
retrieved, err := s.service.GetByID(user.ID)
s.NoError(err)
s.Equal(user.Name, retrieved.Name)
}
// Run the suite
func TestUserServiceTestSuite(t *testing.T) {
suite.Run(t, new(UserServiceTestSuite))
}通过初始化/清理步骤组织相关测试:
go
import (
"testing"
"github.com/stretchr/testify/suite"
)
type UserServiceTestSuite struct {
suite.Suite
db *sql.DB
service *UserService
}
// SetupSuite在所有测试前运行一次
func (s *UserServiceTestSuite) SetupSuite() {
s.db = setupTestDatabase()
s.service = NewUserService(s.db)
}
// TearDownSuite在所有测试后运行一次
func (s *UserServiceTestSuite) TearDownSuite() {
s.db.Close()
}
// SetupTest在每个测试前运行
func (s *UserServiceTestSuite) SetupTest() {
cleanDatabase(s.db)
}
// TearDownTest在每个测试后运行
func (s *UserServiceTestSuite) TearDownTest() {
// 如有需要进行清理
}
// 测试方法必须以"Test"开头
func (s *UserServiceTestSuite) TestCreateUser() {
user := &User{Name: "Alice", Email: "alice@example.com"}
err := s.service.Create(user)
s.NoError(err)
s.NotEqual(0, user.ID) // ID已分配
}
func (s *UserServiceTestSuite) TestGetUser() {
// 初始化
user := &User{Name: "Bob", Email: "bob@example.com"}
s.service.Create(user)
// 测试
retrieved, err := s.service.GetByID(user.ID)
s.NoError(err)
s.Equal(user.Name, retrieved.Name)
}
// 运行测试套件
func TestUserServiceTestSuite(t *testing.T) {
suite.Run(t, new(UserServiceTestSuite))
}Gomock Interface Mocking
Gomock接口模拟
Installation
安装
bash
go install github.com/golang/mock/mockgen@latestbash
go install github.com/golang/mock/mockgen@latestGenerate Mocks
生成模拟对象
go
// user_repository.go
package repository
//go:generate mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks
type UserRepository interface {
GetByID(id int) (*User, error)
Create(user *User) error
Update(user *User) error
Delete(id int) error
}Generate mocks:
bash
go generate ./...go
// user_repository.go
package repository
//go:generate mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks
type UserRepository interface {
GetByID(id int) (*User, error)
Create(user *User) error
Update(user *User) error
Delete(id int) error
}生成模拟对象:
bash
go generate ./...Or manually:
或手动执行:
mockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks
undefinedmockgen -source=user_repository.go -destination=mocks/mock_user_repository.go -package=mocks
undefinedUsing Mocks in Tests
在测试中使用模拟对象
go
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"myapp/repository/mocks"
)
func TestUserService_GetUser(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// Create mock
mockRepo := mocks.NewMockUserRepository(ctrl)
// Set expectations
expectedUser := &User{ID: 1, Name: "Alice"}
mockRepo.EXPECT().
GetByID(1).
Return(expectedUser, nil).
Times(1)
// Test
service := NewUserService(mockRepo)
user, err := service.GetUser(1)
// Assertions
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
}
func TestUserService_CreateUser_Validation(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockUserRepository(ctrl)
// Expect Create to NOT be called (validation should fail first)
mockRepo.EXPECT().Create(gomock.Any()).Times(0)
service := NewUserService(mockRepo)
err := service.CreateUser(&User{Name: ""}) // Invalid user
assert.Error(t, err)
assert.Contains(t, err.Error(), "name is required")
}go
import (
"testing"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
"myapp/repository/mocks"
)
func TestUserService_GetUser(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// 创建模拟对象
mockRepo := mocks.NewMockUserRepository(ctrl)
// 设置预期调用
expectedUser := &User{ID: 1, Name: "Alice"}
mockRepo.EXPECT().
GetByID(1).
Return(expectedUser, nil).
Times(1)
// 测试
service := NewUserService(mockRepo)
user, err := service.GetUser(1)
// 断言
assert.NoError(t, err)
assert.Equal(t, expectedUser, user)
}
func TestUserService_CreateUser_Validation(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockUserRepository(ctrl)
// 预期Create不会被调用(验证应先失败)
mockRepo.EXPECT().Create(gomock.Any()).Times(0)
service := NewUserService(mockRepo)
err := service.CreateUser(&User{Name: ""}) // 无效用户
assert.Error(t, err)
assert.Contains(t, err.Error(), "name is required")
}Custom Matchers
自定义匹配器
go
// Custom matcher for complex validation
type userMatcher struct {
expectedEmail string
}
func (m userMatcher) Matches(x interface{}) bool {
user, ok := x.(*User)
if !ok {
return false
}
return user.Email == m.expectedEmail
}
func (m userMatcher) String() string {
return "matches user with email: " + m.expectedEmail
}
func UserWithEmail(email string) gomock.Matcher {
return userMatcher{expectedEmail: email}
}
// Usage in test
func TestCustomMatcher(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockUserRepository(ctrl)
mockRepo.EXPECT().
Create(UserWithEmail("alice@example.com")).
Return(nil)
service := NewUserService(mockRepo)
service.CreateUser(&User{Name: "Alice", Email: "alice@example.com"})
}go
// 用于复杂验证的自定义匹配器
type userMatcher struct {
expectedEmail string
}
func (m userMatcher) Matches(x interface{}) bool {
user, ok := x.(*User)
if !ok {
return false
}
return user.Email == m.expectedEmail
}
func (m userMatcher) String() string {
return "matches user with email: " + m.expectedEmail
}
func UserWithEmail(email string) gomock.Matcher {
return userMatcher{expectedEmail: email}
}
// 在测试中使用
func TestCustomMatcher(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockUserRepository(ctrl)
mockRepo.EXPECT().
Create(UserWithEmail("alice@example.com")).
Return(nil)
service := NewUserService(mockRepo)
service.CreateUser(&User{Name: "Alice", Email: "alice@example.com"})
}Benchmark Testing
基准测试
Basic Benchmarks
基础基准测试
go
func BenchmarkAdd(b *testing.B) {
calc := NewCalculator()
for i := 0; i < b.N; i++ {
calc.Add(2, 3)
}
}
func BenchmarkStringConcatenation(b *testing.B) {
b.Run("plus operator", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "hello" + "world"
}
})
b.Run("strings.Builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
sb.WriteString("hello")
sb.WriteString("world")
_ = sb.String()
}
})
}go
func BenchmarkAdd(b *testing.B) {
calc := NewCalculator()
for i := 0; i < b.N; i++ {
calc.Add(2, 3)
}
}
func BenchmarkStringConcatenation(b *testing.B) {
b.Run("plus operator", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "hello" + "world"
}
})
b.Run("strings.Builder", func(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
sb.WriteString("hello")
sb.WriteString("world")
_ = sb.String()
}
})
}Running Benchmarks
运行基准测试
bash
undefinedbash
undefinedRun all benchmarks
运行所有基准测试
go test -bench=.
go test -bench=.
Run specific benchmark
运行指定基准测试
go test -bench=BenchmarkAdd
go test -bench=BenchmarkAdd
With memory allocation stats
包含内存分配统计
go test -bench=. -benchmem
go test -bench=. -benchmem
Compare benchmarks
对比基准测试结果
go test -bench=. -benchmem > old.txt
go test -bench=. -benchmem > old.txt
Make changes
进行代码修改
go test -bench=. -benchmem > new.txt
benchstat old.txt new.txt
undefinedgo test -bench=. -benchmem > new.txt
benchstat old.txt new.txt
undefinedBenchmark Output Example
基准测试输出示例
BenchmarkAdd-8 1000000000 0.25 ns/op 0 B/op 0 allocs/op
BenchmarkStringBuilder-8 50000000 28.5 ns/op 64 B/op 1 allocs/opReading: iterations, per operation, bytes allocated per op, allocations per op
5000000028.5 ns/op64 B/op1 allocs/opBenchmarkAdd-8 1000000000 0.25 ns/op 0 B/op 0 allocs/op
BenchmarkStringBuilder-8 50000000 28.5 ns/op 64 B/op 1 allocs/op输出解读:次迭代,每次操作耗时,每次操作分配内存,每次操作分配次内存。
5000000028.5 ns/op64 B/op1 allocs/opAdvanced Testing Patterns
高级测试模式
httptest for HTTP Handlers
用于HTTP处理器的httptest
go
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestUserHandler(t *testing.T) {
handler := http.HandlerFunc(UserHandler)
req := httptest.NewRequest("GET", "/users/1", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Alice")
}
func TestHTTPClient(t *testing.T) {
// Mock server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/users", r.URL.Path)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"id": 1, "name": "Alice"}`))
}))
defer server.Close()
// Test client against mock server
client := NewAPIClient(server.URL)
user, err := client.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
}go
import (
"net/http"
"net/http/httptest"
"testing"
)
func TestUserHandler(t *testing.T) {
handler := http.HandlerFunc(UserHandler)
req := httptest.NewRequest("GET", "/users/1", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, http.StatusOK, rec.Code)
assert.Contains(t, rec.Body.String(), "Alice")
}
func TestHTTPClient(t *testing.T) {
// 模拟服务器
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assert.Equal(t, "/api/users", r.URL.Path)
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"id": 1, "name": "Alice"}`))
}))
defer server.Close()
// 针对模拟服务器测试客户端
client := NewAPIClient(server.URL)
user, err := client.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
}Race Detector
竞态检测器
Detect data races in concurrent code:
bash
go test -race ./...Example test for concurrent safety:
go
func TestConcurrentMapAccess(t *testing.T) {
cache := NewSafeCache()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
cache.Set(fmt.Sprintf("key%d", val), val)
}(i)
}
wg.Wait()
assert.Equal(t, 100, cache.Len())
}检测并发代码中的数据竞态:
bash
go test -race ./...并发安全测试示例:
go
func TestConcurrentMapAccess(t *testing.T) {
cache := NewSafeCache()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(val int) {
defer wg.Done()
cache.Set(fmt.Sprintf("key%d", val), val)
}(i)
}
wg.Wait()
assert.Equal(t, 100, cache.Len())
}Golden File Testing
黄金文件测试
Test against expected output files:
go
func TestRenderTemplate(t *testing.T) {
output := RenderTemplate("user", User{Name: "Alice"})
goldenFile := "testdata/user_template.golden"
if *update {
// Update golden file: go test -update
os.WriteFile(goldenFile, []byte(output), 0644)
}
expected, err := os.ReadFile(goldenFile)
require.NoError(t, err)
assert.Equal(t, string(expected), output)
}
var update = flag.Bool("update", false, "update golden files")与预期输出文件进行对比测试:
go
func TestRenderTemplate(t *testing.T) {
output := RenderTemplate("user", User{Name: "Alice"})
goldenFile := "testdata/user_template.golden"
if *update {
// 更新黄金文件:执行go test -update
os.WriteFile(goldenFile, []byte(output), 0644)
}
expected, err := os.ReadFile(goldenFile)
require.NoError(t, err)
assert.Equal(t, string(expected), output)
}
var update = flag.Bool("update", false, "更新黄金文件")CI/CD Integration
CI/CD集成
GitHub Actions Example
GitHub Actions示例
yaml
undefinedyaml
undefined.github/workflows/test.yml
.github/workflows/test.yml
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.out
- name: Check coverage threshold
run: |
go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//' | \
awk '{if ($1 < 80) exit 1}'undefinedname: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.23'
- name: Run tests
run: go test -v -race -coverprofile=coverage.out ./...
- name: Upload coverage
uses: codecov/codecov-action@v3
with:
files: ./coverage.out
- name: Check coverage threshold
run: |
go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//' | \
awk '{if ($1 < 80) exit 1}'undefinedCoverage Enforcement
覆盖率强制检查
bash
undefinedbash
undefinedGenerate coverage report
生成覆盖率报告
go test -coverprofile=coverage.out ./...
go test -coverprofile=coverage.out ./...
View coverage in terminal
在终端查看覆盖率
go tool cover -func=coverage.out
go tool cover -func=coverage.out
Generate HTML report
生成HTML报告
go tool cover -html=coverage.out -o coverage.html
go tool cover -html=coverage.out -o coverage.html
Check coverage threshold (fail if < 80%)
检查覆盖率阈值(若低于80%则失败)
go test -coverprofile=coverage.out ./... &&
go tool cover -func=coverage.out | grep total | awk '{if (substr($3, 1, length($3)-1) < 80) exit 1}'
go tool cover -func=coverage.out | grep total | awk '{if (substr($3, 1, length($3)-1) < 80) exit 1}'
undefinedgo test -coverprofile=coverage.out ./... &&
go tool cover -func=coverage.out | grep total | awk '{if (substr($3, 1, length($3)-1) < 80) exit 1}'
go tool cover -func=coverage.out | grep total | awk '{if (substr($3, 1, length($3)-1) < 80) exit 1}'
undefinedDecision Trees
决策树
When to Use Each Testing Tool
何时使用各测试工具
Use Standard Package When:
testing- Simple unit tests with few assertions
- No external dependencies to mock
- Performance benchmarking
- Minimal dependencies preferred
Use Testify When:
- Need readable assertions (vs verbose checks)
assert.Equal - Test suites with setup/teardown
- Multiple similar test cases
- Prefer expressive test code
Use Gomock When:
- Testing code with interface dependencies
- Need precise call verification (times, order)
- Complex mock behavior with multiple scenarios
- Type-safe mocking required
Use Benchmarks When:
- Optimizing performance-critical code
- Comparing algorithm implementations
- Detecting performance regressions
- Memory allocation profiling
Use httptest When:
- Testing HTTP handlers
- Mocking external HTTP APIs
- Integration testing HTTP clients
- Testing middleware chains
Use Race Detector When:
- Writing concurrent code
- Using goroutines and channels
- Shared state across goroutines
- CI/CD for all concurrent code
当以下情况时使用标准包:
testing- 带有少量断言的简单单元测试
- 无需模拟外部依赖
- 性能基准测试
- 偏好最小化依赖
当以下情况时使用Testify:
- 需要可读性强的断言(vs 冗长的检查)
assert.Equal - 带有初始化/清理步骤的测试套件
- 多个相似的测试用例
- 偏好表达性强的测试代码
当以下情况时使用Gomock:
- 测试带有接口依赖的代码
- 需要精确的调用验证(次数、顺序)
- 带有多场景的复杂模拟行为
- 需要类型安全的模拟
当以下情况时使用基准测试:
- 优化性能关键代码
- 对比算法实现
- 检测性能回归
- 内存分配分析
当以下情况时使用httptest:
- 测试HTTP处理器
- 模拟外部HTTP API
- 集成测试HTTP客户端
- 测试中间件链
当以下情况时使用竞态检测器:
- 编写并发代码
- 使用goroutine和通道
- goroutine间共享状态
- 对所有并发代码执行CI/CD检查
Anti-Patterns to Avoid
需避免的反模式
❌ Don't Mock Everything
go
// WRONG: Over-mocking makes tests brittle
mockLogger := mocks.NewMockLogger(ctrl)
mockConfig := mocks.NewMockConfig(ctrl)
mockMetrics := mocks.NewMockMetrics(ctrl)
// Too many mocks = fragile test✅ Do: Mock Only External Dependencies
go
// CORRECT: Mock only database, use real logger/config
mockRepo := mocks.NewMockUserRepository(ctrl)
service := NewUserService(mockRepo, realLogger, realConfig)❌ Don't Test Implementation Details
go
// WRONG: Testing internal state
assert.Equal(t, "processing", service.internalState)✅ Do: Test Public Behavior
go
// CORRECT: Test observable outcomes
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)❌ Don't Ignore Error Cases
go
// WRONG: Only testing happy path
func TestGetUser(t *testing.T) {
user, _ := service.GetUser(1) // Ignoring error!
assert.NotNil(t, user)
}✅ Do: Test Error Conditions
go
// CORRECT: Test both success and error cases
func TestGetUser_NotFound(t *testing.T) {
user, err := service.GetUser(999)
assert.Error(t, err)
assert.Nil(t, user)
assert.Contains(t, err.Error(), "not found")
}❌ 不要模拟所有依赖
go
// 错误:过度模拟会导致测试脆弱
mockLogger := mocks.NewMockLogger(ctrl)
mockConfig := mocks.NewMockConfig(ctrl)
mockMetrics := mocks.NewMockMetrics(ctrl)
// 过多模拟 = 脆弱的测试✅ 正确做法:仅模拟外部依赖
go
// 正确:仅模拟数据库,使用真实的日志/配置
mockRepo := mocks.NewMockUserRepository(ctrl)
service := NewUserService(mockRepo, realLogger, realConfig)❌ 不要测试实现细节
go
// 错误:测试内部状态
assert.Equal(t, "processing", service.internalState)✅ 正确做法:测试公开行为
go
// 正确:测试可观察的结果
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)❌ 不要忽略错误场景
go
// 错误:仅测试正常路径
func TestGetUser(t *testing.T) {
user, _ := service.GetUser(1) // 忽略错误!
assert.NotNil(t, user)
}✅ 正确做法:测试错误场景
go
// 正确:同时测试成功和失败路径
func TestGetUser_NotFound(t *testing.T) {
user, err := service.GetUser(999)
assert.Error(t, err)
assert.Nil(t, user)
assert.Contains(t, err.Error(), "not found")
}Best Practices
最佳实践
- Colocate Tests: Place files alongside source code
*_test.go - Use Subtests: Organize related tests with
t.Run() - Parallel When Safe: Enable for independent tests
t.Parallel() - Mock Interfaces: Design for testability with interface dependencies
- Test Errors: Verify both success and failure paths
- Benchmark Critical Paths: Profile performance-sensitive code
- Run Race Detector: Always use for concurrent code
-race - Enforce Coverage: Set minimum thresholds in CI (typically 80%)
- Use Golden Files: Test complex outputs with expected files
- Keep Tests Fast: Mock slow operations, use flag for quick runs
-short
- 测试文件同目录放置: 将文件与源代码放在一起
*_test.go - 使用子测试: 使用组织相关测试
t.Run() - 安全时启用并行: 为独立测试启用
t.Parallel() - 模拟接口: 基于接口设计以提升可测试性
- 测试错误: 验证成功和失败路径
- 基准测试关键路径: 分析性能敏感代码
- 运行竞态检测器: 对并发代码始终使用参数
-race - 强制覆盖率: 在CI中设置最低阈值(通常为80%)
- 使用黄金文件: 对复杂输出使用预期文件测试
- 保持测试快速: 模拟慢速操作,使用参数快速运行
-short
Resources
资源
Official Documentation:
- Go Testing Package: https://pkg.go.dev/testing
- Table-Driven Tests: https://github.com/golang/go/wiki/TableDrivenTests
- Subtests and Sub-benchmarks: https://go.dev/blog/subtests
Testing Frameworks:
- Testify: https://github.com/stretchr/testify
- Gomock: https://github.com/golang/mock
- httptest: https://pkg.go.dev/net/http/httptest
Recent Guides (2025):
- "Go Unit Testing: Structure & Best Practices" (November 2025)
- Go Wiki: CommonMistakes in Testing
- Google Go Style Guide - Testing: https://google.github.io/styleguide/go/
Related Skills:
- golang-engineer: Core Go patterns and concurrency
- verification-before-completion: Testing as part of "done"
- testing-anti-patterns: Avoid common testing mistakes
官方文档:
- Go Testing Package: https://pkg.go.dev/testing
- Table-Driven Tests: https://github.com/golang/go/wiki/TableDrivenTests
- Subtests and Sub-benchmarks: https://go.dev/blog/subtests
测试框架:
- Testify: https://github.com/stretchr/testify
- Gomock: https://github.com/golang/mock
- httptest: https://pkg.go.dev/net/http/httptest
最新指南(2025年):
- 《Go单元测试:结构与最佳实践》(2025年11月)
- Go Wiki: 测试中的常见错误
- Google Go风格指南 - 测试: https://google.github.io/styleguide/go/
相关技能:
- golang-engineer: 核心Go模式与并发
- verification-before-completion: 作为“完成”标准的测试
- testing-anti-patterns: 避免常见测试错误
Quick Reference
快速参考
Run Tests
运行测试
bash
go test ./... # All tests
go test -v ./... # Verbose output
go test -short ./... # Skip slow tests
go test -run TestUserCreate # Specific test
go test -race ./... # With race detector
go test -cover ./... # With coverage
go test -coverprofile=c.out ./... # Coverage file
go test -bench=. -benchmem # Benchmarks with memorybash
go test ./... # 所有测试
go test -v ./... # 详细输出
go test -short ./... # 跳过慢速测试
go test -run TestUserCreate # 指定测试
go test -race ./... # 启用竞态检测器
go test -cover ./... # 启用覆盖率统计
go test -coverprofile=c.out ./... # 生成覆盖率文件
go test -bench=. -benchmem # 带内存统计的基准测试Generate Mocks
生成模拟对象
bash
go generate ./... # All //go:generate directives
mockgen -source=interface.go -destination=mock.gobash
go generate ./... # 所有//go:generate指令
mockgen -source=interface.go -destination=mock.goCoverage Analysis
覆盖率分析
bash
go tool cover -func=coverage.out # Coverage per function
go tool cover -html=coverage.out # HTML reportToken Estimate: ~4,500 tokens (entry point + full content)
Version: 1.0.0
Last Updated: 2025-12-03
bash
go tool cover -func=coverage.out # 按函数查看覆盖率
go tool cover -html=coverage.out # 生成HTML报告Token估算: ~4,500个Token(入口点+完整内容)
版本: 1.0.0
最后更新: 2025-12-03