3、断言与错误处理:标准库assert的局限、使用第三方assert包、自定义错误信息、Errorf与Fatalf的区别

写测试,说白了就是在验证两件事:代码跑没跑通,以及结果对不对。结果对不对这件事,就靠断言来判断。Go 标准库其实没给咱们提供专门的 assert 函数,只给了 t.Errort.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,是因为两个属性检查是独立的,我希望看到所有失败点,而不是修好一个才发现另一个也错了。

注意:不要滥用 Fatalf。如果你在循环里用 Fatalf,第一次失败就退出了,后面的测试数据全被跳过。我曾经见过一个同事,在数据驱动测试里用了 Fatalf,结果 10 组测试数据只跑了 3 组就停了,排查了半天才发现是 Fatalf 的问题。

总结一下

断言和错误处理这块,我的建议很简单:

  • 日常写测试:直接用 testify/assert,省时省力
  • 关键路径:加自定义消息,方便排查
  • 前置条件:用 Fatalf 或 require,避免无效执行
  • 独立断言:用 Errorf 或 assert,收集所有失败点

记住,测试代码也是代码,它需要被阅读、被维护。写清楚断言,就是对自己和同事的尊重。