Go 测试覆盖率入门:数字之外更重要的是关键路径

本文讲解 Go 测试覆盖率的基本命令、HTML 报告、覆盖率误区和关键业务路径测试策略,帮助初学者更有效地使用覆盖率。

覆盖率是信号,不是目标本身

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 看具体分支。它能帮你发现测试盲区,但不能替你判断测试质量。

好的测试应该覆盖关键路径、边界条件和错误路径。覆盖率是地图,不是终点。用它找到没测到的地方,再用有意义的断言补上,才是最实际的做法。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页