Go 单元测试与基准测试实战:从 testing 包到表驱动与 mock 的完整实践
Go 单元测试与基准测试实战:从 testing 包到表驱动与 mock 的完整实践
单元测试是 Go 工程文化里最深入人心的一环——官方工具链 go test 开箱即用,配合 testing 包能覆盖测试、基准、示例、覆盖率的全链路。本文从基础语法讲到表驱动测试、测试替身、并发安全测试与基准分析,帮你写出值得信赖的测试代码。
一、testing 包基础
Go 测试文件以 _test.go 结尾,与源码同目录。测试函数签名固定为 func TestXxx(t *testing.T):
// add.go
func Add(a, b int) int { return a + b }
// add_test.go
package main
import "testing"
func TestAdd(t *testing.T) {
got := Add(1, 2)
if got != 3 {
t.Errorf("Add(1,2) = %d, want 3", got)
}
}
常用断言方式:
t.Errorf:打印错误但继续执行(适合一轮测多个断言);t.Fatalf:立即终止当前测试;t.Logf:打印日志(只有失败或加-v时显示);t.Helper():标记辅助函数,报错时定位到调用方而不是辅助函数内部。
运行方式:
go test ./... # 全包测试
go test -v -run TestAdd # 只跑指定测试
go test -cover # 覆盖率
二、表驱动测试:Go 社区的黄金标准
与其一个用例写一个函数,不如用"表"组织输入输出,一个测试函数跑完所有用例:
func TestAddTable(t *testing.T) {
cases := []struct {
name string
a, b int
want int
}{
{"正数", 1, 2, 3},
{"负数", -1, -2, -3},
{"零值", 0, 0, 0},
{"溢出边界", int(^uint(0)>>1), 1, int(^uint(0)>>1) + 1},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := Add(c.a, c.b); got != c.want {
t.Errorf("Add(%d,%d) = %d, want %d", c.a, c.b, got, c.want)
}
})
}
}
t.Run 创建子测试,好处:
- 失败时能精确定位是哪个用例(
--- FAIL: TestAddTable/正数); - 可以
go test -run 'TestAddTable/负数'单独跑某个用例; - 支持并行子测试(
t.Parallel())。
表驱动 + 子测试是 Go 官方认可的测试范式,几乎适用于所有纯函数和逻辑分支测试。
三、测试替身:mock 与 fake
被测代码依赖外部资源(数据库、HTTP 服务、文件系统)时,有两种策略:
1. 接口注入 + fake 实现(推荐)
把依赖抽象成接口,测试时注入轻量实现:
type UserRepo interface {
Get(id int) (*User, error)
}
type service struct {
repo UserRepo
}
// 测试里的 fake
type fakeRepo struct{}
func (fakeRepo) Get(id int) (*User, error) {
if id == 1 {
return &User{ID: 1, Name: "小卒子"}, nil
}
return nil, errors.New("not found")
}
func TestGetUser(t *testing.T) {
svc := &service{repo: fakeRepo{}}
u, err := svc.Get(1)
if err != nil || u.Name != "小卒子" {
t.Fatalf("unexpected: %v, %v", u, err)
}
}
这就是 Go 社区推崇"面向接口编程"的原因——依赖注入让测试不用引入任何 mock 库。
2. 网络依赖用 httptest
httptest.NewServer 起一个真实 HTTP 服务器,测试 HTTP 客户端逻辑:
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(`{"ok":true}`))
}))
defer srv.Close()
resp, _ := http.Get(srv.URL)
标准库 net/http/httptest 还提供 httptest.NewRecorder 测试 handler,不用起端口就能验证路由响应。
四、并发代码的测试
goroutine 和 channel 的错误大多发生在并发场景,用竞态检测器抓:
go test -race ./...
-race 基于 ThreadSanitizer,能检测 data race,生产项目 CI 里必须开启。测试并发逻辑时还要控制确定性:
func TestWorker(t *testing.T) {
var wg sync.WaitGroup
done := make(chan struct{})
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 工作
}()
}
go func() { wg.Wait(); close(done) }()
select {
case <-done:
case <-time.After(2 * time.Second):
t.Fatal("worker 超时,可能死锁")
}
}
给并发测试加超时保护,避免死锁时测试挂死整个 CI。
五、基准测试:Benchmark 的正确姿势
基准函数签名 func BenchmarkXxx(b *testing.B),框架自动决定迭代次数:
func BenchmarkBuilder(b *testing.B) {
for i := 0; i < b.N; i++ {
var sb strings.Builder
for j := 0; j < 100; j++ {
sb.WriteString("x")
}
}
}
运行 go test -bench=. -benchmem,输出:
BenchmarkBuilder-8 385240 3124 ns/op 0 B/op 0 allocs/op
解读字段:385240 迭代次数、3124 ns/op 单次耗时、0 B/op 每次分配字节、0 allocs/op 每次分配次数。allocs/op 是 Go 性能优化最重要的指标——减少分配通常比减少耗时更有效,因为 GC 压力是全局的。
对比两个实现时用 -benchtime=3s 增加采样时间,且两个 benchmark 要在同一台机器、同一状态下跑,才能比较。还想稳定比较可以加 -count=5 取多次均值。
b.ResetTimer() 可以在 setup 之后才开始计时;b.ReportAllocs() 或 -benchmem 输出分配信息。
六、覆盖率与 Example 测试
覆盖率分析
go test -coverprofile=cover.out ./...
go tool cover -func=cover.out # 按函数看覆盖率
go tool cover -html=cover.out # 浏览器可视化
覆盖率是手段不是目标——分支覆盖率比行覆盖率更有意义。对关键逻辑(金额计算、权限判断、协议解析)要求覆盖所有 if/else 分支,而不是只追求数字好看。
Example 测试:可执行的文档
func ExampleAdd() {
fmt.Println(Add(1, 2))
// Output: 3
}
Example 测试同时是文档示例和断言测试,// Output: 注释会被比对输出。它会被 godoc 渲染到文档里,是"文档即测试"的典范。
七、实践清单
- 表驱动 + 子测试组织所有用例,命名用业务语义("正数""空输入")而非序号;
- 依赖一律接口注入,测试用 fake 而非重连真实服务;
- CI 里
go test -race -cover ./...是底线; - 被测代码把边界条件(零值、nil、超长、溢出)都写进表;
- 性能优化前后用 benchmem 对比 allocs/op,用数据说话;
- 错误处理测试覆盖
%w包装链路,用errors.Is/As断言根因。
测试的价值不在"写了多少行",而在替你挡下了多少线上事故。Go 把测试工具内置到工具链、文档、覆盖率全链路,这套机制配合"测试优先"的工程文化,正是 Go 在云原生领域被广泛采用的重要原因之一。
读完了?给作者一点支持
🔗 相关文章
Go 并发编程实战:从 Goroutine、Channel 到 Context 的完整指南
系统梳理 Go 并发的核心机制:GMP 调度模型、Channel 与 CSP、select 高级用法、sync 工具箱、Context 生命周期管理,以及 Worker Pool、Pipeline、Fan-in/Fan-out 三大模式,并给出 goroutine 泄漏与数据竞争的排查方法和最佳实践清单。
Redis 高可用架构实战:缓存穿透、击穿、雪崩的全面防御
深入解析缓存穿透、击穿、雪崩三大高发事故的本质,给出空值缓存、布隆过滤器、互斥锁、逻辑过期、随机 TTL、多级缓存等可落地防御方案与高可用演进路径。
构建高质量 RESTful API:设计规范与安全实践
从资源建模、状态码语义、JWT 认证鉴权到接口安全十大防护点,再到版本演进策略,给出可直接落地的 RESTful API 设计与安全实践清单。
评论区加载中…