5.2 测试与基准测试:交接班验收制度


5.2 测试与基准测试:交接班验收制度

本节摘要:测试是交接班的验收单:功能测试证对错,竞态测试保并发,基准测试量性能。本节覆盖表驱动测试惯例、并发组件的测试策略与基准测试的完整工作流。读完本节,你对任何一处改动的第一反应会是"跑一遍测试",而不是"应该没问题"。

上一节把代码摆进了该在的位置,本节解决改动的底气问题。先反问一句:没有测试的代码,你敢在半夜上线前改吗?不敢的根源不是代码复杂,而是没有验收制度——改完无法证明"还是对的"。Go 把测试做进了工具链,写测试的摩擦低到没有借口,本节把这套验收制度完整过一遍。

表驱动测试:一套用到底的惯例

测试文件以测试结尾,与被测代码同目录;测试函数签名固定为 func TestXxx(t *testing.T)。Go 社区最常用的组织方式是表驱动:用例列表加循环断言,加用例只需加一行:

package payroll import "testing" func TestNightPay(t *testing.T) { cases := []struct { name string hourly int hours float64 deepNight bool want float64 }{ {"白班八小时", 50, 8, false, 400}, {"夜班八小时", 50, 8, true, 480}, {"零工时", 50, 0, true, 0}, {"高额时薪夜班", 200, 2, true, 480}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { // 子测试:每个用例独立报告 got := NightPay(c.hourly, c.hours, c.deepNight) if got != c.want { t.Errorf("NightPay(%d, %v, %v) = %v, 期望 %v", c.hourly, c.hours, c.deepNight, got, c.want) } }) } } // 会话: // $ go test -v ./internal/payroll/ // === RUN TestNightPay // === RUN TestNightPay/白班八小时 // === RUN TestNightPay/夜班八小时 // --- PASS: TestNightPay (0.00s)

子测试的好处在实际排障时显现:失败报告精确到用例名,不用再从一串日志里找是哪组输入错了。浮点比较记得用误差范围而不是等号,这是表驱动用例里最常见的坑。

测并发代码:仪器必须在场

并发组件的功能测试与普通代码无异,区别在于必须始终开着竞态检测器跑,以及要测"错误路径"而不只是"快乐路径"。给 4.1 的工作池写一个验收测试:

package scheduler import ( "sync" "testing" ) func TestWorkerPoolProcessesAll(t *testing.T) { jobs := make(chan int, 32) results := make(chan int, 32) var wg sync.WaitGroup for w := 0; w < 4; w++ { wg.Add(1) go func() { defer wg.Done() for j := range jobs { results <- j * 2 } }() } const n = 100 go func() { for j := 1; j <= n; j++ { jobs <- j } close(jobs) }() go func() { wg.Wait() close(results) }() seen := map[int]bool{} for r := range results { seen[r]++ } if len(seen) != n { t.Fatalf("结果数 %d, 期望 %d", len(seen), n) } for j := 1; j <= n; j++ { if !seen[j*2] { t.Fatalf("任务 %d 的结果缺失", j) } } } // 会话: // $ go test -race ./internal/scheduler/ // PASS // 竞态检测器全程在场:测试通过即代表运行期内未出现无同步并发访问

三个并发测试的经验值得记录:断言"结果集完整"而不是"输出顺序"——并发组件的行为契约是无丢失、无重复,顺序本就不属于契约;测试里不加 sleep,靠 channel 与 WaitGroup 精确同步,sleep 只会让测试又慢又飘;竞态检测器发现的报警即使"这次结果是对的"也必须修,它的判据是时序可能性而不是本次运气。

基准测试:性能改动的验收单

4.3 已经用基准做裁判,本节补全工作流:写基准、对比、用统计工具判断差异是否可信。

package scheduler import "testing" func BenchmarkDispatch(b *testing.B) { b.Run("串行派发", func(b *testing.B) { for i := 0; i < b.N; i++ { dispatch(1) } }) b.Run("并行派发", func(b *testing.B) { b.RunParallel(func(pb *testing.PB) { for pb.Next() { dispatch(1) } }) }) } // 会话: // $ go test -bench . -count 6 ./internal/scheduler/ | tee old.txt // (改动代码) // $ go test -bench . -count 6 ./internal/scheduler/ | tee new.txt // $ benchstat old.txt new.txt // │ old.txt │ new.txt │ // │ sec/op │ sec/op vs base │ // 串行派发 112.5n ± 2% 109.8n ± 1% -2.40% (p=0.002 n=6)

count 多轮加 benchstat 统计,是为了把"看起来快了"升级成"在统计意义上快了,且提升幅度大于波动"。性能评审时没有这两步的对比截图,一律视为未测。内存维度用 b.ReportAllocs 打开,allocs/op 的下降往往比 ns/op 更能说明 GC 压力的缓解。

测什么:三条覆盖底线

  • 错误路径与快乐路径同权重:并发组件的失败分支(下游超时、队满丢弃)恰恰是事故高发区,用例数量至少对半。
  • 边界与饱和:缓冲满、并发度上限、任务数为零或极小,饱和行为是背压设计的直接验证。
  • 可测性靠设计:依赖外部资源的组件用接口注入假实现(第 2 章的接口在此兑现),测试里不连真库、不发真请求。

完整案例:给批处理任务补一套验收

背景:5.1 接手的调度服务,批处理模块零测试,改动全靠胆量。

操作:三步补测——先给核心分发函数补表驱动功能测试;再给工作池补并发契约测试并全程开竞态检测;最后给分发热点补基准,纳入 CI。全程没有改动一行业务代码。

结果:第一轮就抓到一个真实的锁粒度问题(整批任务包在临界区里),基准显示修复后并行吞吐提升约四成;此后模块改动全部先跑测试,半夜上线前的心态从"赌"变成"验"。

解读:这个案例的顺序是刻意安排的:功能测试立对错,竞态测试立安全,基准立性能,三者缺一,验收就是漏检。零测试的老模块先补功能测试还是先补基准?答案是功能——对错都保证不了,谈性能没有意义。

变式:为流水线组件补一个"混沌测试":随机让下游慢响应、随机失败,断言不丢任务且最终一致。写完你会发现,混沌测试逼着你把 4.5 手册里的每个修复都变成可执行的断言——测试写到位的团队,手册都是薄的。

收班要点

  • 表驱动加子测试是 Go 功能测试的标准形态,失败报告精确到用例名。
  • 并发测试三原则:结果集契约而非顺序、不用 sleep 用同步原语、竞态检测常开
  • 性能结论要有统计效力:count 多轮加 benchstat,看分布不看单次。
  • 覆盖底线:错误路径对半、饱和行为必测、依赖注入保可测。
  • 验收顺序:功能、竞态、性能,逐级补齐,不可跳跃。

代码验得动了,下一节看它的口粮从哪来:依赖管理,把供应链的名单管清楚。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U