前言
“没测试的代码 = 不知道能不能跑的代码”。Go 把测试做成语言级公民:go test 开箱即用,没有选择困难。这篇从规范到进阶一次讲清。
一、最小测试
规则:测试文件以 _test.go 结尾,函数签名 func TestXxx(t *testing.T)。
// split.go
package strutil
import "strings"
func Split(s, sep string) []string {
return strings.Split(s, sep)
}
// split_test.go
package strutil
import "testing"
func TestSplit(t *testing.T) {
got := Split("a,b,c", ",")
want := []string{"a", "b", "c"}
if len(got) != len(want) {
t.Errorf("长度不符: got %d, want %d", len(got), len(want))
}
for i := range want {
if got[i] != want[i] {
t.Errorf("第%d个元素: got %q, want %q", i, got[i], want[i])
}
}
}
go test ./... # 跑当前模块全部测试
go test -v # 显示每个用例
go test -run TestSplit # 只跑匹配的
二、表驱动测试(Go 的招牌写法)
一组输入输出写成表,循环断言——新增用例只加一行:
func TestSplit(t *testing.T) {
tests := []struct {
name string
input string
sep string
want []string
}{
{"普通逗号", "a,b,c", ",", []string{"a", "b", "c"}},
{"无分隔符", "abc", ",", []string{"abc"}},
{"空字符串", "", ",", []string{""}},
{"分隔符在两端", ",a,", ",", []string{"", "a", ""}},
{"中文", "你好,世界", ",", []string{"你好", "世界"}},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { // 子测试: 单独报告、单独运行
got := Split(tt.input, tt.sep)
if !slices.Equal(got, tt.want) {
t.Errorf("Split(%q, %q) = %v, want %v",
tt.input, tt.sep, got, tt.want)
}
})
}
}
go test -v -run TestSplit/中文 # 只跑某个子用例
三、testify:让断言好看点
go get github.com/stretchr/testify
import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSplit(t *testing.T) {
got := Split("a,b,c", ",")
assert.Equal(t, []string{"a", "b", "c"}, got) // 失败继续跑完
require.Len(t, got, 3, "应切出3段") // 失败立即终止(防后续 panic)
assert.Contains(t, got, "b")
assert.NoError(t, err)
}
assert(失败继续)vs require(失败即停):后者用于"后面依赖前面结果"的场景。
四、覆盖率
go test -cover ./...
# ok demo/strutil 0.012s coverage: 85.7% of statements
# 生成 HTML 报告, 看哪些行没测到
go test -coverprofile=cover.out ./...
go tool cover -html=cover.out -o cover.html
# CI 里卡阈值(低于80%失败)
go test -cover ./... | awk -F'coverage: ' '/coverage/ {gsub("%.*","",$2); if ($2+0 < 80) exit 1}'
覆盖率是体检指标不是 KPI:80% 且关键路径全测 远胜于刷到 95% 的无脑断言。
五、Benchmark 基准测试
func BenchmarkXxx(b *testing.B),框架自动调 b.N 次直到测量稳定:
func BenchmarkSplit(b *testing.B) {
for i := 0; i < b.N; i++ { // 循环体必须是 b.N 次
Split("a,b,c,d,e,f,g,h", ",")
}
}
go test -bench=. -benchmem ./...
# BenchmarkSplit-8 5824932 204.1 ns/op 112 B/op 1 allocs/op
# │ │ │ └ 每次操作堆分配次数
# │ │ └ 每次操作分配字节数
# │ └ 单次操作耗时
# └ -8 = GOMAXPROCS
优化对比实战:
// 优化前: 每次都分配
func Join(b []byte, s string) string { return string(b) + s }
// 优化后: 预分配
func Join2(b []byte, s string) string {
out := make([]byte, 0, len(b)+len(s))
out = append(out, b...)
out = append(out, s...)
return string(out)
}
func BenchmarkJoin(b *testing.B) { for i := 0; i < b.N; i++ { Join(x, "y") } }
func BenchmarkJoin2(b *testing.B) { for i := 0; i < b.N; i++ { Join2(x, "y") } }
// BenchmarkJoin-8 8ns/op 16B/op 1 allocs/op
# BenchmarkJoin2-8 5ns/op 0B/op 0 allocs/op ← 预分配立功
六、并行测试与超时
func TestParallel(t *testing.T) {
tests := loadCases() // 100 个用例
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel() // 子用例并行跑(大幅提速)
got := heavy(tt.input)
assert.Equal(t, tt.want, got)
})
}
}
go test -timeout 60s ./... # 全局超时(默认10min, CI必配)
go test -race ./... # 竞态检测(测试并发代码必开!)
七、测外部依赖:接口 + mock
依赖外部服务时,先抽象成接口(结构体接口篇的招数),测试时注入假实现:
type Notifier interface {
Notify(title, body string) error
}
type fakeNotifier struct {
calls int
err error
}
func (f *fakeNotifier) Notify(t, b string) error {
f.calls++
return f.err
}
func TestAlerter(t *testing.T) {
fn := &fakeNotifier{err: errors.New("network down")}
a := NewAlerter(fn)
err := a.Fire("cpu high")
require.Error(t, err) // 通知失败要有正确行为
assert.Equal(t, 1, fn.calls) // 且真的调用过一次
}
gomock/testify mock 是这套思路的自动化版本——但能手写 fake 时优先手写,可读性更好。
八、CI 集成(GitLab CI 示例)
test:
stage: test
image: golang:1.22
script:
- go vet ./... # 静态检查
- go test -race -coverprofile=cover.out -timeout 120s ./...
- go tool cover -func=cover.out | tail -1
coverage: '/coverage: \d+\.\d+/'
九、常见坑
| 现象 | 原因 |
|---|---|
no required module provides package |
测试文件 package 声明错了;或没 go mod init |
| benchmark 数值乱跳 | 机器忙/电源管理;多跑几次取稳定值,加 -benchtime=3s |
| 测试顺序依赖互相污染 | 用例要幂等;共享状态用 setup/teardown 隔离 |
| t.Parallel 后 data race | 闭包变量被并发改;循环变量传参(老版本 Go) |
| mock 之后测了个寂寞 | 假实现直接返回"想要的"——mock 测的是交互,真集成要 e2e 补 |
小结
| 需求 | 工具 |
|---|---|
| 基本测试 | TestXxx(t *testing.T) |
| 多用例 | 表驱动 + t.Run 子测试 |
| 断言 | testify assert/require |
| 覆盖率 | -coverprofile + HTML |
| 性能 | -bench + -benchmem |
| 并发正确性 | -race |
| 外部依赖 | 接口 + 手写 fake |
本文是「Golang」系列第 9 篇。