高覆盖率≠高质量:数据表明70%-80%后边际效益骤降,100%覆盖可能遗漏关键错误路径、边界条件及异常分支,真正需关注的是未执行的高危逻辑而非百分比本身。

覆盖率数字高不等于代码质量高,但低于 70% 通常意味着关键错误路径、边界输入或依赖异常没被验证。真正要补的不是“行数”,而是那些 if err != nil、len(s) == 0、ctx.Err() != nil 这类一旦跳过就可能线上 panic 的分支。
用 go tool cover -func 定位真正该补的函数
别只看 go test -cover 输出的百分比。那个数字掩盖了严重不均衡:某个工具函数覆盖 100%,而核心 handler 里 db.QueryRow 失败后直接 log.Fatal 的那三行永远红着。
-
go test -coverprofile=coverage.out生成数据后,立刻跑go tool cover -func=coverage.out - 重点关注
coverage: 0.0%或33.3%这类低值函数——它们往往包含未触发的else、return err或defer清理逻辑 - 避免用
-covermode=count初期排查;set模式(默认)更快暴露“是否执行”,比“执行几次”更贴近测试意图
错误路径必须显式构造,不能靠“运气”触发
Go 的错误处理是显式的,但多数测试只传合法参数、mock 成功返回,导致 if err != nil 块完全没进过。这些分支恰恰是日志、回滚、降级逻辑所在。
- 对每个返回
error的外部调用,mock 必须至少写两条:一条成功(Return(user, nil)),一条失败(Return(nil, errors.New("timeout"))) - HTTP handler 测试里,用
httptest.NewRequest("GET", "/user/0", nil)强制触发 ID 解析失败;用bytes.NewReader([]byte(""))模拟空 JSON body - 注意
nil切片和空切片区别:var s []string = nil和s := []string{}在len(s) == 0后行为可能不同,都要测
用表格驱动 + context 控制超时/取消分支
带 context.Context 参数的函数,手动 time.Sleep 等超时既慢又不稳定,还容易漏掉 cancel 场景。
- 表格驱动测试中,为 context 分支单独加 case:
{"ctx cancelled", func() context.Context { ctx, cancel := context.WithCancel(context.Background()); cancel(); return ctx }(), true} - 超时分支用
context.WithTimeout(context.Background(), 1*time.Millisecond),确保 select 中的case 必然命中 - 避免在测试里启动 goroutine 后用
time.Sleep等结果;改用sync.WaitGroup或chan struct{}显式同步
拆函数 + 依赖注入,让不可控逻辑可测
直接在函数里调 time.Now()、rand.Intn() 或全局 db 实例,会导致该函数几乎无法写出稳定、快速的单元测试。
- 把时间/随机逻辑抽成函数变量或接口方法,测试时替换为固定返回值:
nowFunc := time.Now; ... nowFunc() - 数据库操作不要写
db := getDB(),改为接收DBer interface{ QueryRow(...)} 参数 - HTTP handler 不要自己解析 body,把解码逻辑提到独立函数,接收
io.Reader,测试时传strings.NewReader(`{"id":1}`)
最常被忽略的是:覆盖率报告里的红色行,往往不是“难写测试”,而是“没意识到这行代码需要被验证”。比如一个 defer file.Close() 下面跟着 if err != nil { log.Printf("close failed: %v", err) } ——这个 log 分支,90% 的测试根本不会让它执行,但它在线上磁盘满时就是唯一线索。











