3、断言与错误处理:标准库assert的局限、使用第三方assert包、自定义错误信息、Errorf与Fatalf的区别
写测试,说白了就是在验证两件事:代码跑没跑通,以及结果对不对。结果对不对这件事,就靠断言来判断。Go 标准库其实没给咱们提供专门的 assert 函数,只给了 t.Error、t.Fatal 这些基础工具。嗯,这里就有个问题了——用原生方式写断言,代码会变得很啰嗦。
标准库 assert 的局限
先看个例子。假设你要测试一个加法函数:
func TestAdd(t *testing.T) {
result := Add(2, 3)
if result != 5 {
t.Errorf("期望 %d, 实际得到 %d", 5, result)
}
}
这段代码没问题,但你看,每次都要写 if result != expected 这种模板代码。一个测试文件里如果有几十个断言,光这些 if 语句就够你受的。我在项目中遇到过,一个同事写了 200 行的测试,其中 150 行都是这种 if 判断。维护起来简直想哭。
标准库的局限说白了就两点:
- 啰嗦:每个断言都要手动写条件判断和错误信息
- 信息不直观:错误信息得自己拼,容易漏掉关键值
使用 testify/assert 包
这时候就该 github.com/stretchr/testify 出场了。我个人习惯,只要写单元测试,第一件事就是 go get 这个包。它提供的 assert 包能让你少写 70% 的模板代码。
同样的测试,用 testify 写是这样的:
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestAdd(t *testing.T) {
result := Add(2, 3)
assert.Equal(t, 5, result, "加法结果不对")
}
一行搞定。而且它自动帮你生成清晰的错误信息:
=== RUN TestAdd
test_test.go:10:
Error Trace: test_test.go:10
Error: Not equal:
expected: 5
actual : 6
Message: 加法结果不对
Test: TestAdd
--- FAIL: TestAdd (0.00s)
你看,它把期望值、实际值、调用位置、自定义消息全给你列出来了。调试的时候一目了然。
testify 还提供了很多常用的断言函数:
| 函数 | 作用 |
|---|---|
assert.Equal |
判断两个值相等 |
assert.NotEqual |
判断两个值不相等 |
assert.Nil |
判断值为 nil |
assert.NotNil |
判断值不为 nil |
assert.True |
判断条件为 true |
assert.False |
判断条件为 false |
assert.Contains |
判断切片或字符串包含某个元素 |
assert.Len |
判断长度 |
require 包代替 assert。require 的用法和 assert 完全一样,区别在于 require 失败时会直接 t.FatalNow,而 assert 只是 t.Error。
自定义错误信息
testify 的断言函数都支持最后一个参数作为自定义消息。我个人习惯,在关键业务逻辑的测试里,一定会加上自定义消息。为什么?因为团队协作时,别人看你的测试报告,能立刻知道哪里出了问题。
assert.Equal(t, expectedUser, actualUser,
"用户信息不匹配,用户ID: %d", userID)
这里用了格式化字符串,把变量值也带进去。我曾经因为没加自定义消息,排查一个线上 bug 花了整整一下午。后来发现是测试用例里的期望值写错了。如果当时加了自定义消息,一眼就能看出来。
自定义消息的格式建议:
- 先说「是什么」:比如「用户信息不匹配」
- 再说「关键值」:比如用户ID、订单号
- 不要写废话:比如「测试失败了」这种毫无信息量的消息
Errorf 与 Fatalf 的区别
这两个函数是标准库提供的,也是 testify 底层依赖的基础。理解它们的区别,能帮你写出更健壮的测试。
| 函数 | 行为 | 适用场景 |
|---|---|---|
t.Errorf |
标记测试失败,但继续执行后续代码 | 多个独立的断言,希望看到所有失败点 |
t.Fatalf |
标记测试失败,并立即停止当前测试 | 前置条件失败,后续断言没有意义 |
举个例子你就明白了:
func TestUserCreation(t *testing.T) {
// 第一步:创建用户
user, err := CreateUser("alice")
if err != nil {
t.Fatalf("创建用户失败: %v", err) // 这里用 Fatalf
}
// 第二步:检查用户属性
if user.Name != "alice" {
t.Errorf("用户名错误,期望 alice,实际 %s", user.Name)
}
if user.Age != 0 {
t.Errorf("新用户年龄应为 0,实际 %d", user.Age)
}
}
为什么第一步用 Fatalf?因为如果用户都没创建成功,后面的属性检查毫无意义。你想想看,一个 nil 指针去访问 user.Name,直接 panic 了。而第二步用 Errorf,是因为两个属性检查是独立的,我希望看到所有失败点,而不是修好一个才发现另一个也错了。
总结一下
断言和错误处理这块,我的建议很简单:
- 日常写测试:直接用 testify/assert,省时省力
- 关键路径:加自定义消息,方便排查
- 前置条件:用 Fatalf 或 require,避免无效执行
- 独立断言:用 Errorf 或 assert,收集所有失败点
记住,测试代码也是代码,它需要被阅读、被维护。写清楚断言,就是对自己和同事的尊重。