覆盖率是信号,不是目标本身
Go 自带测试覆盖率工具。你可以很容易知道哪些代码被测试执行过,哪些没有。这对补测试很有帮助,但覆盖率数字本身并不等于质量。80% 覆盖率可能没有测到最关键的支付路径,40% 覆盖率也可能把核心业务规则保护得很好。
入门阶段学习覆盖率,应该把它当成一个发现盲区的工具,而不是拿来追求漂亮数字。你要关心的是:错误路径有没有测?边界条件有没有测?核心规则有没有测?外部依赖失败时有没有测?
这篇文章讲 Go 覆盖率命令和实际使用方式。
查看覆盖率
运行:
go test -cover ./...
输出类似:
ok example.com/app/user 0.021s coverage: 78.4% of statements
生成覆盖率文件:
go test -coverprofile=coverage.out ./...
查看函数级覆盖:
go tool cover -func=coverage.out
生成 HTML:
go tool cover -html=coverage.out
HTML 报告会用颜色标出哪些语句被覆盖。它非常适合发现某个错误分支从来没测过。
一个需要补测试的函数
func NormalizePage(page, size int) (int, int) {
if page <= 0 {
page = 1
}
if size <= 0 {
size = 20
}
if size > 100 {
size = 100
}
return page, size
}
表驱动测试:
func TestNormalizePage(t *testing.T) {
tests := []struct {
name string
page int
size int
wantPage int
wantSize int
}{
{"valid", 2, 30, 2, 30},
{"default page", 0, 30, 1, 30},
{"default size", 1, 0, 1, 20},
{"cap size", 1, 200, 1, 100},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
gotPage, gotSize := NormalizePage(tt.page, tt.size)
if gotPage != tt.wantPage || gotSize != tt.wantSize {
t.Fatalf("got (%d,%d), want (%d,%d)",
gotPage, gotSize, tt.wantPage, tt.wantSize)
}
})
}
}
这类规则函数很适合用覆盖率辅助检查。每个分支都应该有用例。
覆盖率的误区
第一,执行到不代表断言正确。下面测试可能提高覆盖率,但没有验证结果:
func TestCreateUser(t *testing.T) {
CreateUser("bad-email")
}
它只是调用了函数,没有检查错误是否符合预期。
第二,追求 100% 可能不划算。有些代码是简单组装、日志分支、很薄的 main 函数,测试价值有限。优先测业务规则、错误处理和边界。
第三,覆盖率不能代表集成正确。单元测试覆盖了 SQL 构造,不代表真实数据库迁移没问题;覆盖了 HTTP handler,不代表反向代理配置正确。
给关键路径设置最低线
如果团队想使用覆盖率门槛,不要一开始就追求全项目高数字。更实际的做法是先保护核心包,比如订单、支付、权限、配置解析。你可以先查看包级覆盖率:
go test -cover ./internal/order ./internal/config
也可以在 CI 脚本里读取总覆盖率,但要小心不要让数字游戏压过测试质量。很多生成代码、简单 DTO、main 函数会拉低覆盖率,却未必值得花很多时间测试。
更好的策略是:关键业务包设较高目标,边缘组装代码保持基本测试。每次修 bug 时补一条回归测试。这样覆盖率会随着真实风险逐步增长,而不是为了达标写一堆没有断言的测试。
覆盖错误路径
错误路径经常比成功路径更重要。比如配置读取:
func TestLoadConfigMissingDatabaseURL(t *testing.T) {
t.Setenv("DATABASE_URL", "")
_, err := LoadConfig()
if err == nil {
t.Fatal("expected error")
}
}
HTTP handler:
func TestCreateUserInvalidJSON(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/users", strings.NewReader("{bad"))
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if rec.Code != http.StatusBadRequest {
t.Fatalf("status = %d", rec.Code)
}
}
覆盖率报告能帮你发现这些分支有没有执行,但断言仍然要靠你写清楚。
从报告里挑最值得补的地方
打开 HTML 覆盖率报告后,不要看到红色就机械补测试。先找这些区域:业务判断密集的函数、错误转换函数、权限判断、金额计算、配置校验、状态机流转。它们的风险更高,测试收益也更高。
比如一个订单状态判断:
func CanCancel(status string, paid bool) bool {
if status == "closed" {
return false
}
if paid {
return false
}
return status == "pending"
}
这类函数看起来短,但规则非常关键。它应该有清楚的表驱动测试:
func TestCanCancel(t *testing.T) {
tests := []struct {
status string
paid bool
want bool
}{
{"pending", false, true},
{"pending", true, false},
{"closed", false, false},
}
for _, tt := range tests {
got := CanCancel(tt.status, tt.paid)
if got != tt.want {
t.Fatalf("CanCancel(%q,%v) = %v", tt.status, tt.paid, got)
}
}
}
这比给一段简单 getter 补测试更有价值。覆盖率工具告诉你哪里没执行,工程判断决定先测哪里。
小结前再记一条
覆盖率数字很容易被滥用。真正健康的团队会看失败用例是否清楚、关键规则是否被保护、测试是否稳定快速,而不是只看百分比。你可以把覆盖率作为代码审查的提示:这次改了关键逻辑,测试有没有跟上?如果没有,就补有意义的用例。
这比单纯追数字更可靠。
如果你在团队里推动测试覆盖率,可以先从“新代码必须有测试”开始,而不是要求旧项目立刻达到某个高比例。老代码补测试需要时间,强行设高门槛容易让大家写低质量测试凑数。更健康的方式是:新功能带测试,修 bug 带回归测试,核心包逐步提高覆盖率。
这样覆盖率增长会慢一点,但每一条测试都更有实际价值。
小结
Go 覆盖率工具很轻:go test -cover 看概况,-coverprofile 生成报告,go tool cover -html 看具体分支。它能帮你发现测试盲区,但不能替你判断测试质量。
好的测试应该覆盖关键路径、边界条件和错误路径。覆盖率是地图,不是终点。用它找到没测到的地方,再用有意义的断言补上,才是最实际的做法。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。