跳到主要内容

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 引入的特性,通过随机生成的输入来发现程序中的漏洞。

基本概念

模糊测试会:

  1. 使用初始语料库(seed corpus)运行测试
  2. 生成随机变异的输入
  3. 利用代码覆盖率指导,找到更有价值的输入
  4. 发现问题时保存导致失败的输入

编写模糊测试

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 的测试机制:

  1. 单元测试:使用 testing 包编写测试,go test 运行测试
  2. 表驱动测试:组织测试数据,减少重复代码
  3. 子测试和并行测试:使用 t.Runt.Parallel() 组织测试
  4. 基准测试:测量代码性能,使用 b.Loop()b.N
  5. 模糊测试:使用随机输入发现程序漏洞
  6. 测试覆盖率:使用 -cover 查看和生成覆盖率报告
  7. Mock 测试:使用接口和 testify 进行依赖隔离
  8. 最佳实践:命名清晰、测试独立、测试行为而非实现

练习

  1. 为一个包含多种方法的类型编写完整的表驱动测试
  2. 使用 testify 重写测试,包括断言和 Mock
  3. 编写一个基准测试,比较两种不同实现的性能
  4. 为一个解析函数编写模糊测试
  5. 创建一个测试套件,包含 Setup 和 Teardown

参考资料