Skip to content
← 返回Go列表
知识专题

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:

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:

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 注释是可执行检查,不是普通说明。基准只测量,不代替这些正确性断言。

依据:testing、b.Loop、竞态检测。

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 需要工具链/平台支持,也只观察实际执行的路径;本页仅推演,不执行测试。

书面自检:单独通过、整体失败时是否先放宽断言?提示:比较测试夹具、全局状态和固定端口。

参考答案

先定位共享状态和顺序依赖,记录失败输入及首个错误。放宽断言会隐藏问题;先修正隔离与所有权,再判断业务预期是否改变。

继续阅读