Go 测试
测试是保证代码质量的重要手段。Go 语言内置了轻量级的测试框架 testing 包,配合 go test 命令,可以方便地编写和运行测试。本章将系统介绍 Go 的测试机制,从基础的单元测试到高级的模糊测试。
测试基础
测试文件命名规则
Go 的测试文件必须满足以下规则:
- 文件名以
_test.go结尾 - 测试文件与被测试的代码在同一个包(或者使用
_test后缀的外部测试包) - 测试函数名以
Test开头,参数为*testing.T
// math.go
package math
func Add(a, b int) int {
return a + b
}
func Subtract(a, b int) int {
return a - b
}
// math_test.go
package math
import "testing"
func TestAdd(t *testing.T) {
result := Add(2, 3)
expected := 5
if result != expected {
t.Errorf("Add(%d, %d) = %d; want %d", 2, 3, result, expected)
}
}
func TestSubtract(t *testing.T) {
result := Subtract(5, 3)
expected := 2
if result != expected {
t.Errorf("Subtract(%d, %d) = %d; want %d", 5, 3, result, expected)
}
}
解释:测试文件 math_test.go 与被测试代码在同一个 math 包中,可以直接访问包内的函数。
运行测试
使用 go test 命令运行测试:
# 运行当前包的所有测试
go test
# 显示详细输出
go test -v
# 运行特定测试(支持正则表达式)
go test -run TestAdd
# 运行所有包的测试
go test ./...
# 显示测试覆盖率
go test -cover
# 生成覆盖率报告
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
# 运行基准测试
go test -bench=.
# 检测竞态条件
go test -race
测试函数的结构
一个测试函数通常包含三个部分:准备(Arrange)、执行(Act)、断言(Assert)。
func TestAdd(t *testing.T) {
// 准备:设置测试数据
a, b := 2, 3
expected := 5
// 执行:调用被测试的函数
result := Add(a, b)
// 断言:验证结果
if result != expected {
t.Errorf("Add(%d, %d) = %d; want %d", a, b, result, expected)
}
}
解释:这种结构使测试代码清晰易读,也称为 AAA 模式。
testing.T 的常用方法
testing.T 提供了多种方法来报告测试结果:
func TestTMethods(t *testing.T) {
// 记录日志(仅在测试失败或 -v 模式下显示)
t.Log("这是一条日志")
t.Logf("格式化日志: %d", 42)
// 标记失败但继续执行
t.Fail()
t.Errorf("Errorf 会标记失败但继续执行")
// 标记失败并立即停止当前测试
t.FailNow()
t.Fatalf("Fatalf 会标记失败并停止执行")
t.SkipNow()
t.Skip("跳过当前测试")
t.Skipf("跳过测试: %s", "原因")
// 标记为帮助函数(在错误报告中跳过)
t.Helper()
}
方法分类:
| 方法 | 行为 | 用途 |
|---|---|---|
Log/Logf | 记录日志 | 调试信息 |
Error/Errorf | 标记失败,继续执行 | 非致命错误 |
Fatal/Fatalf | 标记失败,停止执行 | 致命错误 |
Skip/Skipf/SkipNow | 跳过测试 | 跳过不适用的情况 |
表驱动测试
表驱动测试是 Go 中最常用的测试模式,它将测试数据组织成表格,避免重复代码。
基本结构
func TestAdd(t *testing.T) {
// 定义测试用例表
tests := []struct {
name string // 测试用例名称
a, b int // 输入参数
expected int // 期望结果
}{
{"正数相加", 2, 3, 5},
{"负数相加", -1, -1, -2},
{"正负相加", -5, 10, 5},
{"零值", 0, 0, 0},
{"大数相加", 1000000, 2000000, 3000000},
}
// 遍历测试用例
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
解释:t.Run 创建子测试,每个测试用例独立运行,失败时能清楚看到是哪个用例失败。
运行特定子测试
# 运行 TestAdd 下的所有子测试
go test -run TestAdd
# 运行特定子测试
go test -run "TestAdd/正数相加"
# 使用正则匹配
go test -run "TestAdd/正.*"
包含错误情况的表驱动测试
func TestDivide(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
expectErr bool
}{
{"正常除法", 10, 2, 5, false},
{"除数为零", 10, 0, 0, true},
{"整数除法", 7, 2, 3, false},
{"负数除法", -10, 2, -5, false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := Divide(tt.a, tt.b)
if tt.expectErr {
if err == nil {
t.Error("期望返回错误,但没有返回")
}
} else {
if err != nil {
t.Errorf("不期望返回错误: %v", err)
}
if result != tt.expected {
t.Errorf("Divide(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
}
})
}
}
子测试与并行测试
子测试(Subtests)
使用 t.Run 创建子测试,实现测试的组织和复用:
func TestUser(t *testing.T) {
t.Run("Create", func(t *testing.T) {
// 测试用户创建
})
t.Run("Update", func(t *testing.T) {
// 测试用户更新
})
t.Run("Delete", func(t *testing.T) {
// 测试用户删除
})
}
并行测试
使用 t.Parallel() 标记测试可以并行执行:
func TestParallel(t *testing.T) {
t.Run("A", func(t *testing.T) {
t.Parallel() // 标记为可并行
// 测试 A
})
t.Run("B", func(t *testing.T) {
t.Parallel()
// 测试 B
})
t.Run("C", func(t *testing.T) {
// 测试 C 不并行
})
}
解释:t.Parallel() 会暂停当前测试,等待其他并行测试就绪后一起执行。
并行测试的等待机制
func TestGroupedParallel(t *testing.T) {
// 这组测试会并行执行
t.Run("group", func(t *testing.T) {
tests := []struct {
name string
data int
}{
{"test1", 1},
{"test2", 2},
{"test3", 3},
}
for _, tt := range tests {
tt := tt // 捕获变量
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
// 测试逻辑
})
}
})
// 这里会等待 group 中的所有并行测试完成
t.Log("group 中的测试已完成")
}
测试辅助函数
Helper 函数
使用 t.Helper() 标记辅助函数,错误报告时会跳过辅助函数的调用位置:
// 断言相等的辅助函数
func assertEqual(t *testing.T, got, expected interface{}, msg ...string) {
t.Helper() // 标记为辅助函数
if got != expected {
if len(msg) > 0 {
t.Errorf("%s: got %v, want %v", msg[0], got, expected)
} else {
t.Errorf("got %v, want %v", got, expected)
}
}
}
func TestWithHelper(t *testing.T) {
result := Add(2, 3)
assertEqual(t, result, 5, "Add 结果") // 错误会指向这一行,而不是 assertEqual 内部
}
解释:没有 t.Helper() 时,错误会报告在 assertEqual 函数内部,这会让调试变得困难。
Cleanup 函数
使用 t.Cleanup() 注册清理函数,在测试完成后自动执行:
func TestWithCleanup(t *testing.T) {
// 创建临时文件
file, err := os.CreateTemp("", "test")
if err != nil {
t.Fatal(err)
}
// 注册清理函数
t.Cleanup(func() {
os.Remove(file.Name())
t.Log("临时文件已清理")
})
// 使用文件进行测试...
}
func TestWithDatabase(t *testing.T) {
db := setupDatabase()
t.Cleanup(func() {
db.Close()
})
// 测试数据库操作...
}
解释:无论测试成功还是失败,清理函数都会执行。多个清理函数按 LIFO(后进先出)顺序执行。
TempDir 方法
Go 1.15+ 提供了 t.TempDir() 方法,自动创建和清理临时目录:
func TestWithTempDir(t *testing.T) {
// 创建临时目录,测试结束后自动删除
tempDir := t.TempDir()
// 在临时目录中创建文件
filename := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(filename, []byte("hello"), 0644)
if err != nil {
t.Fatal(err)
}
// 测试...
}
Skip 方法
在某些条件下跳过测试:
func TestSlowOperation(t *testing.T) {
// 短测试模式跳过
if testing.Short() {
t.Skip("跳过慢速测试")
}
// 执行耗时的测试...
}
func TestNetworkCall(t *testing.T) {
if os.Getenv("NETWORK_TEST") == "" {
t.Skip("设置 NETWORK_TEST=1 启用网络测试")
}
// 执行网络测试...
}
运行方式:
# 短测试模式,跳过标记为慢的测试
go test -short
# 启用网络测试
NETWORK_TEST=1 go test
基准测试
基准测试用于测量代码的性能,函数名以 Benchmark 开头,参数为 *testing.B。
基本结构
func BenchmarkAdd(b *testing.B) {
// b.N 会自动调整,确保测试运行足够长的时间
for b.Loop() {
Add(2, 3)
}
}
// 或者使用旧的 b.N 风格
func BenchmarkAddOld(b *testing.B) {
for i := 0; i < b.N; i++ {
Add(2, 3)
}
}
解释:推荐使用 b.Loop()(Go 1.24+),它会自动管理计时器,比 b.N 风格更可靠。
运行基准测试
# 运行所有基准测试
go test -bench=.
# 运行特定基准测试
go test -bench=BenchmarkAdd
# 显示内存分配统计
go test -bench=. -benchmem
# 指定运行时间
go test -bench=. -benchtime=5s
# 指定运行次数
go test -bench=. -benchtime=100x
# 运行多次以获得更稳定的结果
go test -bench=. -count=5
基准测试输出解读
$ go test -bench=. -benchmem
BenchmarkAdd-8 68453040 17.8 ns/op 0 B/op 0 allocs/op
输出说明:
BenchmarkAdd-8:测试名称,-8表示 GOMAXPROCS 数68453040:执行次数17.8 ns/op:每次操作耗时0 B/op:每次操作分配的内存0 allocs/op:每次操作的内存分配次数
计时器控制
当基准测试需要准备工作时,需要手动控制计时器:
func BenchmarkBigOperation(b *testing.B) {
// 准备工作(不计入基准时间)
data := generateLargeData()
// 重置计时器
b.ResetTimer()
for b.Loop() {
process(data)
}
}
func BenchmarkComplex(b *testing.B) {
for b.Loop() {
// 停止计时
b.StopTimer()
data := prepareData()
// 开始计时
b.StartTimer()
process(data)
}
}
并行基准测试
func BenchmarkParallel(b *testing.B) {
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
// 并行执行的测试代码
processRequest()
}
})
}
运行:
# 使用不同 CPU 数量
go test -bench=Parallel -cpu=1,2,4,8
自定义指标报告
func BenchmarkCustomMetric(b *testing.B) {
var operations int64
for b.Loop() {
operations++
doSomething()
}
// 报告自定义指标
b.ReportMetric(float64(operations)/float64(b.N), "ops/test")
}
模糊测试(Fuzzing)
模糊测试是 Go 1.18 引入的特性,通过随机生成的输入来发现程序中的漏洞。
基本概念
模糊测试会:
- 使用初始语料库(seed corpus)运行测试
- 生成随机变异的输入
- 利用代码覆盖率指导,找到更有价值的输入
- 发现问题时保存导致失败的输入
编写模糊测试
func FuzzAdd(f *testing.F) {
// 添加种子语料库
f.Add(1, 2)
f.Add(-1, 1)
f.Add(0, 0)
// 定义模糊目标
f.Fuzz(func(t *testing.T, a, b int) {
result := Add(a, b)
// 验证结果
// 加法应该满足交换律
if Add(b, a) != result {
t.Errorf("加法交换律不成立: Add(%d,%d) != Add(%d,%d)", a, b, b, a)
}
// 结果应该等于 a + b
expected := a + b
if result != expected {
t.Errorf("Add(%d, %d) = %d; want %d", a, b, result, expected)
}
})
}
模糊测试的参数类型
模糊测试支持以下类型:
| 类型 | 说明 |
|---|---|
string | 字符串 |
[]byte | 字节切片 |
int, int8, int16, int32, int64 | 有符号整数 |
uint, uint8, uint16, uint32, uint64 | 无符号整数 |
float32, float64 | 浮点数 |
bool | 布尔值 |
解析器模糊测试示例
func FuzzJSONParse(f *testing.F) {
// 添加有效的 JSON 作为种子
f.Add(`{"name": "test", "value": 123}`)
f.Add(`[1, 2, 3]`)
f.Add(`"hello"`)
f.Add(`null`)
f.Fuzz(func(t *testing.T, jsonStr string) {
var v interface{}
err := json.Unmarshal([]byte(jsonStr), &v)
// 无效 JSON 可以跳过
if err != nil {
t.Skip() // 跳过无效输入
}
// 重新序列化
result, err := json.Marshal(v)
if err != nil {
t.Fatalf("Marshal failed: %v", err)
}
// 再次解析,应该成功
var v2 interface{}
err = json.Unmarshal(result, &v2)
if err != nil {
t.Fatalf("Remarshal failed: %v", err)
}
})
}
运行模糊测试
# 作为单元测试运行(只运行种子语料库)
go test -run FuzzAdd
# 启用模糊测试
go test -fuzz=FuzzAdd
# 限制模糊测试时间
go test -fuzz=FuzzAdd -fuzztime=30s
# 限制模糊测试次数
go test -fuzz=FuzzAdd -fuzztime=1000x
模糊测试输出
fuzz: elapsed: 0s, gathering baseline coverage: 0/192 completed
fuzz: elapsed: 0s, gathering baseline coverage: 192/192 completed, now fuzzing with 8 workers
fuzz: elapsed: 3s, execs: 325017 (108336/sec), new interesting: 11 (total: 202)
fuzz: elapsed: 6s, execs: 680218 (118402/sec), new interesting: 12 (total: 203)
--- FAIL: FuzzAdd
Failing input written to testdata/fuzz/FuzzAdd/a878c3134fe0404d...
To re-run:
go test -run=FuzzAdd/a878c3134fe0404d...
FAIL
解释:
new interesting:发现了能增加代码覆盖率的新输入- 失败的输入会保存到
testdata/fuzz/目录 - 保存的输入会作为回归测试
测试覆盖率
查看覆盖率
# 显示覆盖率百分比
go test -cover
# 生成覆盖率报告文件
go test -coverprofile=coverage.out
# 查看函数级别覆盖率
go tool cover -func=coverage.out
# 在浏览器中查看
go tool cover -html=coverage.out
覆盖率模式
Go 支持三种覆盖率模式:
# set 模式:是否执行(默认)
go test -covermode=set -coverprofile=coverage.out
# count 模式:执行次数
go test -covermode=count -coverprofile=coverage.out
# atomic 模式:原子计数(适用于并发测试)
go test -covermode=atomic -coverprofile=coverage.out
在测试中获取覆盖率
func TestCoverage(t *testing.T) {
// 获取当前覆盖率
coverage := testing.Coverage()
t.Logf("当前覆盖率: %.2f%%", coverage*100)
// 获取覆盖模式
mode := testing.CoverMode()
t.Logf("覆盖模式: %s", mode)
}
覆盖率最佳实践
- 不要追求 100% 覆盖率:覆盖率只是指标,不是目的
- 关注关键路径:确保核心逻辑有充分测试
- 定期检查覆盖率:监控覆盖率变化趋势
- 结合其他测试:覆盖率不能替代代码审查和集成测试
Mock 和 Stub 测试
当测试依赖外部系统(数据库、API 等)时,需要使用 Mock 或 Stub 来隔离测试。
接口抽象
首先,定义接口来抽象依赖:
// 定义数据存储接口
type UserRepository interface {
FindByID(id int) (*User, error)
Save(user *User) error
Delete(id int) error
}
// 真实实现
type DBUserRepository struct {
db *sql.DB
}
func (r *DBUserRepository) FindByID(id int) (*User, error) {
// 数据库查询实现
}
// 服务依赖接口
type UserService struct {
repo UserRepository
}
func (s *UserService) GetUser(id int) (*User, error) {
return s.repo.FindByID(id)
}
手动 Mock
手动创建 Mock 实现:
// Mock 实现
type MockUserRepository struct {
Users map[int]*User
}
func (m *MockUserRepository) FindByID(id int) (*User, error) {
user, ok := m.Users[id]
if !ok {
return nil, errors.New("user not found")
}
return user, nil
}
func (m *MockUserRepository) Save(user *User) error {
m.Users[user.ID] = user
return nil
}
func (m *MockUserRepository) Delete(id int) error {
delete(m.Users, id)
return nil
}
// 测试代码
func TestUserService_GetUser(t *testing.T) {
// 创建 Mock
mockRepo := &MockUserRepository{
Users: map[int]*User{
1: {ID: 1, Name: "张三"},
},
}
// 注入 Mock
service := &UserService{repo: mockRepo}
// 测试
user, err := service.GetUser(1)
if err != nil {
t.Fatalf("GetUser failed: %v", err)
}
if user.Name != "张三" {
t.Errorf("user.Name = %s; want 张三", user.Name)
}
// 测试不存在的用户
_, err = service.GetUser(999)
if err == nil {
t.Error("期望返回错误,但没有返回")
}
}
使用 testify/mock
使用 testify 库可以更方便地创建 Mock:
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// Mock 实现
type MockRepository struct {
mock.Mock
}
func (m *MockRepository) FindByID(id int) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func (m *MockRepository) Save(user *User) error {
args := m.Called(user)
return args.Error(0)
}
func TestUserService_GetUser_Mock(t *testing.T) {
mockRepo := new(MockRepository)
service := &UserService{repo: mockRepo}
// 设置期望
expectedUser := &User{ID: 1, Name: "张三"}
mockRepo.On("FindByID", 1).Return(expectedUser, nil)
mockRepo.On("FindByID", 999).Return(nil, errors.New("not found"))
// 测试存在的用户
user, err := service.GetUser(1)
assert.NoError(t, err)
assert.Equal(t, "张三", user.Name)
// 测试不存在的用户
_, err = service.GetUser(999)
assert.Error(t, err)
// 验证所有期望都被满足
mockRepo.AssertExpectations(t)
}
testify 断言包
testify 提供了丰富的断言函数:
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAssertions(t *testing.T) {
// assert:失败后继续执行
assert.Equal(t, 200, 200, "两个值应该相等")
assert.NotEqual(t, 200, 201, "两个值不应该相等")
assert.True(t, true, "应该为 true")
assert.False(t, false, "应该为 false")
assert.Nil(t, nil, "应该为 nil")
assert.NotNil(t, "hello", "不应该为 nil")
assert.Len(t, []int{1, 2, 3}, 3, "切片长度应该为 3")
assert.Contains(t, "hello world", "world", "字符串应该包含子串")
assert.InDelta(t, 3.14, 3.14159, 0.01, "值应该在误差范围内")
// require:失败后立即停止
require.NotNil(t, someValue, "必须不为 nil 才能继续")
// 后续代码只有 someValue 不为 nil 时才会执行
// 使用 assert.New 简化
assert := assert.New(t)
assert.Equal(200, 200)
assert.True(true)
}
assert vs require:
assert:断言失败后继续执行,适合检查多个独立条件require:断言失败后立即停止,适合前置条件检查
testify Suite
使用 Suite 组织测试:
import (
"testing"
"github.com/stretchr/testify/suite"
)
// 定义测试套件
type UserTestSuite struct {
suite.Suite
service *UserService
mockRepo *MockRepository
}
// 每个测试前执行
func (s *UserTestSuite) SetupTest() {
s.mockRepo = new(MockRepository)
s.service = &UserService{repo: s.mockRepo}
}
// 测试方法
func (s *UserTestSuite) TestGetUser() {
expectedUser := &User{ID: 1, Name: "张三"}
s.mockRepo.On("FindByID", 1).Return(expectedUser, nil)
user, err := s.service.GetUser(1)
s.NoError(err)
s.Equal("张三", user.Name)
s.mockRepo.AssertExpectations(s.T())
}
func (s *UserTestSuite) TestGetUserNotFound() {
s.mockRepo.On("FindByID", 999).Return(nil, errors.New("not found"))
_, err := s.service.GetUser(999)
s.Error(err)
}
// 运行套件
func TestUserSuite(t *testing.T) {
suite.Run(t, new(UserTestSuite))
}
Suite 生命周期方法:
SetupSuite():套件开始前执行一次TearDownSuite():套件结束后执行一次SetupTest():每个测试前执行TearDownTest():每个测试后执行BeforeTest(suiteName, testName string):测试前执行AfterTest(suiteName, testName string):测试后执行
TestMain 函数
当需要在所有测试前后执行初始化和清理时,可以使用 TestMain:
func TestMain(m *testing.M) {
// 初始化代码
fmt.Println("测试开始前初始化...")
setupDatabase()
// 运行测试
code := m.Run()
// 清理代码
fmt.Println("测试结束后清理...")
cleanupDatabase()
os.Exit(code)
}
带命令行参数的 TestMain
var testDB *sql.DB
func TestMain(m *testing.M) {
// 解析命令行参数
flag.Parse()
// 初始化测试数据库
var err error
testDB, err = sql.Open("sqlite3", ":memory:")
if err != nil {
fmt.Fprintf(os.Stderr, "无法连接测试数据库: %v\n", err)
os.Exit(1)
}
defer testDB.Close()
// 运行迁移
if err := runMigrations(testDB); err != nil {
fmt.Fprintf(os.Stderr, "迁移失败: %v\n", err)
os.Exit(1)
}
os.Exit(m.Run())
}
示例函数
示例函数既是文档,也是测试:
// 基本示例
func ExampleAdd() {
result := Add(2, 3)
fmt.Println(result)
// Output: 5
}
// 带后缀的示例
func ExampleAdd_negative() {
result := Add(-1, -2)
fmt.Println(result)
// Output: -3
}
// 为类型方法编写示例
func ExampleUser_Name() {
u := User{Name: "张三"}
fmt.Println(u.Name)
// Output: 张三
}
// 无序输出
func ExamplePrintRandom() {
rand.Shuffle(3, func(i, j int) {
// 打乱顺序
})
for i := 0; i < 3; i++ {
fmt.Println(i)
}
// Unordered output: 0
// 1
// 2
}
命名规则:
Example():包示例ExampleF():函数 F 的示例ExampleT():类型 T 的示例ExampleT_M():类型 T 的方法 M 的示例Example_suffix():带后缀的示例
测试最佳实践
1. 测试命名
// 好的命名:描述性强
func TestAdd_PositiveNumbers_ReturnsSum(t *testing.T) {}
func TestDivide_ByZero_ReturnsError(t *testing.T) {}
func TestUser_Create_ValidData_Success(t *testing.T) {}
// 不好的命名:含义不清
func TestAdd1(t *testing.T) {}
func TestDiv(t *testing.T) {}
2. 测试独立性
// 不好:测试之间有依赖
func TestSetup(t *testing.T) {
globalVar = setup()
}
func TestOperation(t *testing.T) {
// 依赖 TestSetup 先运行
useGlobalVar()
}
// 好:每个测试独立
func TestOperation(t *testing.T) {
// 每个测试自己准备数据
data := setup()
result := operation(data)
// 验证结果
}
3. 使用表驱动测试
// 不好:重复代码
func TestAdd_1(t *testing.T) {
if Add(1, 2) != 3 {
t.Error("1 + 2 应该等于 3")
}
}
func TestAdd_2(t *testing.T) {
if Add(-1, 1) != 0 {
t.Error("-1 + 1 应该等于 0")
}
}
// 好:表驱动测试
func TestAdd(t *testing.T) {
tests := []struct {
a, b, expected int
}{
{1, 2, 3},
{-1, 1, 0},
{0, 0, 0},
}
for _, tt := range tests {
if got := Add(tt.a, tt.b); got != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, got, tt.expected)
}
}
}
4. 测试错误路径
func TestDivide(t *testing.T) {
t.Run("正常情况", func(t *testing.T) {
result, err := Divide(10, 2)
require.NoError(t, err)
assert.Equal(t, 5, result)
})
t.Run("除数为零", func(t *testing.T) {
_, err := Divide(10, 0)
assert.Error(t, err)
assert.Contains(t, err.Error(), "zero")
})
t.Run("溢出", func(t *testing.T) {
result, err := Divide(math.MaxInt64, 1)
require.NoError(t, err)
assert.Equal(t, math.MaxInt64, result)
})
}
5. 使用 t.Helper()
// 辅助函数
func assertUserEqual(t *testing.T, got, expected *User) {
t.Helper()
assert.Equal(t, expected.ID, got.ID)
assert.Equal(t, expected.Name, got.Name)
assert.Equal(t, expected.Email, got.Email)
}
// 测试
func TestGetUser(t *testing.T) {
user := GetUser(1)
expected := &User{ID: 1, Name: "张三", Email: "[email protected]"}
assertUserEqual(t, user, expected) // 错误会指向这一行
}
6. 避免测试私有的实现细节
// 不好:测试私有函数
func Test_privateFunction(t *testing.T) {
// 测试私有函数会让重构变得困难
}
// 好:测试公开行为
func TestPublicFunction(t *testing.T) {
// 通过公开 API 测试行为
}
7. 使用构建标签
//go:build integration
package mypackage
import "testing"
func TestDatabase(t *testing.T) {
// 集成测试,需要数据库连接
}
运行:
# 默认跳过集成测试
go test
# 运行集成测试
go test -tags=integration
常用测试命令速查
# 基本命令
go test # 运行当前包测试
go test ./... # 运行所有包测试
go test -v # 详细输出
go test -run TestName # 运行特定测试
# 覆盖率
go test -cover # 显示覆盖率
go test -coverprofile=c.out # 生成覆盖率报告
go tool cover -html=c.out # 查看覆盖率详情
# 基准测试
go test -bench=. # 运行所有基准测试
go test -bench=. -benchmem # 显示内存分配
go test -bench=. -count=5 # 运行 5 次
# 模糊测试
go test -fuzz=FuzzName # 运行模糊测试
go test -fuzztime=30s # 限制时间
# 其他选项
go test -race # 检测竞态条件
go test -short # 短测试模式
go test -timeout=30s # 设置超时
小结
本章系统介绍了 Go 的测试机制:
- 单元测试:使用
testing包编写测试,go test运行测试 - 表驱动测试:组织测试数据,减少重复代码
- 子测试和并行测试:使用
t.Run和t.Parallel()组织测试 - 基准测试:测量代码性能,使用
b.Loop()或b.N - 模糊测试:使用随机输入发现程序漏洞
- 测试覆盖率:使用
-cover查看和生成覆盖率报告 - Mock 测试:使用接口和 testify 进行依赖隔离
- 最佳实践:命名清晰、测试独立、测试行为而非实现
练习
- 为一个包含多种方法的类型编写完整的表驱动测试
- 使用 testify 重写测试,包括断言和 Mock
- 编写一个基准测试,比较两种不同实现的性能
- 为一个解析函数编写模糊测试
- 创建一个测试套件,包含 Setup 和 Teardown