Go 测试:断言、隔离与失败定位
示例、基准与失败分支
前置:表驱动测试、错误包装。单元测试检查行为,Example 检查文档输出,Benchmark 测量指定负载;三者用途不同。Go 1.27.2;本轮只检查测试规格与预期输出,未执行 Test、Example 或 Benchmark。macOS 仅作平台说明,本项目不执行平台运行验证。
完整测试项目
在空目录执行 go mod init example.com/calc,go.mod 使用 go 1.27.0,创建下面两个文件。它们同属 calc 包,不能和 main.go 混放。
calc.go:
package calc
import (
"errors"
"fmt"
)
var ErrZero = errors.New("zero divisor")
func Divide(a, b int) (int, error) {
if b == 0 { return 0, fmt.Errorf("divide %d: %w", a, ErrZero) }
return a / b, nil
}calc_test.go:
package calc
import (
"errors"
"fmt"
"testing"
)
func TestDivide(t *testing.T) {
for _, tc := range []struct { a, b, want int; wantErr error }{
{9, 2, 4, nil}, {-9, 2, -4, nil}, {0, 2, 0, nil}, {1, 0, 0, ErrZero},
} {
got, err := Divide(tc.a, tc.b)
if got != tc.want || !errors.Is(err, tc.wantErr) {
t.Fatalf("Divide(%d,%d)=(%d,%v)", tc.a, tc.b, got, err)
}
}
}
func ExampleDivide() {
value, err := Divide(9, 2)
fmt.Println(value, err)
// Output: 4 <nil>
}
func BenchmarkDivide(b *testing.B) {
for b.Loop() {
if _, err := Divide(9, 2); err != nil { b.Fatal(err) }
}
}运行 go test -v ./...,预期 TestDivide 和 ExampleDivide 均 PASS。将实现故意改为 return a + b, nil,正常值测试和 Example 会失败;恢复后再运行。
基准与分配测量
执行 go test -run '^$' -bench . -benchmem -count=3 .,应看到 BenchmarkDivide、ns/op、B/op、allocs/op 与 PASS。数字随机器和负载变化,不提供固定性能预期。'^$' 是正则,匹配不到普通测试名;PowerShell、Bash、Zsh 都可保留引号。
b.Loop 从 Go 1.24 起提供,负责控制测量循环和计时边界。旧项目可用 for i := 0; i < b.N; i++,同一个基准不要把两种循环嵌套使用。只在相同版本、硬件、输入与负载条件下比较多次结果;这个整数函数不代表数据库或 HTTP 吞吐。
覆盖率、竞态与调试
go test -cover ./... 显示执行过的语句比例,不证明所有边界正确。go test -race ./... 检查本次运行触发的数据竞争,需要受支持的平台及 C 工具链;普通测试通过不能冒充 race 通过。TestMain 处理进程级准备和收尾,分组使用 t.Run,不要求引入第三方 suite。
失败时先用 go test -run TestDivide -count=1 -v . 复现单例,读文件与行号,再用 t.Log 或断点定位;不要用 Sleep 掩盖偶发失败。
练习与自检
将包装符 %w 改为 %v,观察除零用例仍收到错误但 errors.Is 失败;恢复后运行所有测试。再给 Example 故意写错 Output,确认文档输出也会触发失败。
参考解法
错误文案相同不代表错误链相同。Example 的 Output 注释是可执行检查,不是普通说明。基准只测量,不代替这些正确性断言。
Go 测试失败或偶发失败
本轮只做理论正文与源码静态复核;下列命令、示例和测试均未执行。已有输出按代码推演或历史记录理解,不作为本轮运行证据。
先缩小到单个测试,再区分断言、共享状态和数据竞争。
测试失败不一定是被测函数本身错误。先保留完整输出及运行环境;并发或依赖外部服务的测试,还需要核对共享状态和服务可用性。
排查步骤
1. 获得完整失败范围
go test ./...预期结果: 记录失败的 package、测试名和首个错误;编译失败应先解决编译问题。
2. 单独复现失败测试
go test -run '^TestName$' -count=1 -v ./path/to/package预期结果: 排除缓存影响并显示测试日志;若单测通过,再检查并行或顺序依赖。
3. 检查并发数据竞争
go test -race ./path/to/package预期结果: 竞争检测报告指向读写位置;没有报告也不能证明不存在所有并发问题。
常见报错
expected / got 不一致
原因: 断言、测试数据或业务行为发生偏差。
处理方式: 对照输入、实际输出和期望,检查最近代码及测试夹具改动。
单独通过、整体失败
原因: 共享全局状态、并行测试或外部资源冲突。
处理方式: 检查 t.Parallel、全局变量、临时目录和数据库清理;重复运行缩小范围。
相关文档
官方资料
如何解读静态测试规格
TestName 与 ./path/to/package 是替换项;-run 的正则限制测试名,-count=1 不会自动重置共享数据库或其他外部状态。-race 需要工具链/平台支持,也只观察实际执行的路径;本页仅推演,不执行测试。
书面自检:单独通过、整体失败时是否先放宽断言?提示:比较测试夹具、全局状态和固定端口。
参考答案
先定位共享状态和顺序依赖,记录失败输入及首个错误。放宽断言会隐藏问题;先修正隔离与所有权,再判断业务预期是否改变。
继续阅读
- 排错
- 开发实践
- 开发实践