Go测试返回值需用got/want变量显式比较并调用t.Errorf;多返回值须验证error,成功路径用t.Fatalf,失败路径用errors.Is检查;表驱动测试应使用t.Run和结构体字段name/input/want/wantErr;testify/assert适用于深比较但会终止子测试。

测试函数返回值的基本写法(Go test)
Go 原生 testing 包不提供断言函数,验证返回值靠的是显式比较 + t.Errorf。别指望 assert.Equal 那套——那是第三方库的玩法,标准做法是直接写判断逻辑。
例如测试一个加法函数:
func Add(a, b int) int {
return a + b
}
func TestAdd(t *testing.T) {
got := Add(2, 3)
want := 5
if got != want {
t.Errorf("Add(2,3) = %d, want %d", got, want)
}
}
- 必须用
got和want变量命名,这是 Go 社区约定,便于快速识别实际值与期望值 - 错误信息里要同时输出
got和want,否则失败时只能看到一行 “want 5”,根本不知道实际返回了啥 - 别在
if分支里只写t.Fatal——它会立刻终止当前测试,掩盖后续 case 的问题;优先用t.Errorf让所有 case 都跑完
处理多返回值和 error 的常见坑
Go 函数常返回 (result, error),测试时容易漏掉对 error 的检查,或把 nil 当成 “没出错” 就完事。实际上,error 是一等公民,必须显式验证。
比如测试一个可能失败的除法函数:
立即学习“go语言免费学习笔记(深入)”;
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
func TestDivide(t *testing.T) {
// 测试成功路径
got, err := Divide(10, 2)
if err != nil {
t.Fatalf("unexpected error: %v", err) // 这里用 Fatal 合理:成功路径不该有 error
}
if got != 5.0 {
t.Errorf("Divide(10,2) = %f, want 5.0", got)
}
// 测试失败路径
_, err = Divide(10, 0)
if err == nil {
t.Error("expected error for division by zero, got nil")
}
if !strings.Contains(err.Error(), "division by zero") {
t.Errorf("error message doesn't match: %v", err)
}
}
- 成功路径中,先检查
err != nil,再用t.Fatalf中断——因为此时出错属于严重逻辑缺陷 - 失败路径中,不能只检查
err != nil,还要验证错误内容是否符合预期,尤其当函数返回自定义 error 类型时,应优先用errors.Is或errors.As判断 - 别用
reflect.DeepEqual比较 error:它只比内存结构,不比语义;errors.Is才是正确姿势
表驱动测试(table-driven tests)怎么组织返回值验证
当一个函数需要测多个输入组合时,硬写一堆 TestXxx 函数既冗余又难维护。Go 推荐用切片+循环的表驱动方式,每个测试项明确标注 name、input、want、wantErr。
关键点在于:每个子测试要用 t.Run 单独包裹,否则失败时无法定位具体是哪个 case 出问题:
func TestParseInt(t *testing.T) {
tests := []struct {
name string
input string
want int
wantErr bool
}{
{"positive", "42", 42, false},
{"negative", "-7", -7, false},
{"invalid", "abc", 0, true},
{"empty", "", 0, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := strconv.Atoi(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("error mismatch: got error %v, wantErr %t", err, tt.wantErr)
return
}
if !tt.wantErr && got != tt.want {
t.Errorf("got %d, want %d", got, tt.want)
}
})
}
}
- 结构体字段名统一用
name/input/want/wantErr,团队协作时一眼能懂 -
t.Run的第一个参数必须是tt.name,否则go test -run=TestParseInt/positive这类子测试过滤就失效 - 注意括号:判断 error 是否发生要用
(err != nil) != tt.wantErr,而不是err != nil != tt.wantErr(后者语法错误)
什么时候该用 testify/assert 而不是原生 testing
原生写法清晰可控,但写多了容易重复:每行都得写 if got != want { t.Errorf(...) }。当你频繁测试 JSON 序列化、嵌套结构体、slice 元素顺序,或者需要深比较时,testify/assert 的可读性优势就出来了。
但它不是银弹——引入后要注意三点:
- 所有
assert函数(如assert.Equal)内部调用的是t.FailNow,即失败立即终止当前子测试,无法像原生那样继续执行后续校验逻辑 - 如果项目要求零外部依赖,或者 CI 环境禁止非标准库,那就必须坚持原生写法
-
assert.JSONEq对空格和 key 顺序不敏感,适合比对 API 返回,但如果你需要精确匹配格式(比如生成配置文件),就得回退到bytes.Equal或cmp.Diff
复杂结构体的深比较,cmp.Diff(来自 github.com/google/go-cmp/cmp)比 testify/assert 更透明——它会输出详细差异,而不是一句 “not equal”。但代价是多一个 import,且需手动处理 unexported 字段。










