讲解
测试在 Go 里是语言内置能力,不需要任何第三方框架:测试代码写在「被测文件同名加 _test.go」的文件里(如 main.go 对应 main_test.go),测试函数以 Test 开头、接收 *testing.T 参数,运行 go test 即可。断言不需要学习新语法——if got != want { t.Errorf(...) } 就是最标准的写法,直白得近乎朴素,但正因为没有框架魔法,任何人的测试代码你都能秒懂。
表驱动测试是 Go 社区的标志性模式:把测试用例定义成一个结构体切片(名字、输入、期望输出),循环里逐个执行。加一个用例就是加一行数据,几十组输入输出一目了然,比复制粘贴一堆 Test 函数清爽得多。配合 t.Run(c.name, ...) 把每组用例变成子测试,失败时能精确定位是哪一组挂的。
go test 的常用姿势:go test ./... 跑模块下所有包;go test -run TestAdd 只跑匹配的测试;go test -v 显示每个测试的名字与结果;go test -race 带上数据竞争检测;go test -cover 显示覆盖率。写测试的投入产出比极高——尤其是表驱动测试,成本几乎就是多写几行数据。
示例
表驱动测试(这是 main_test.go 文件的内容,用 go test 运行):
package main
import "testing"
func Add(a, b int) int { return a + b }
func TestAdd(t *testing.T) {
cases := []struct {
name string
a, b int
expected int
}{
{"正数相加", 1, 2, 3},
{"含负数", -1, 1, 0},
{"两个零", 0, 0, 0},
{"大数", 1000000, 1, 1000001},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
if got := Add(c.a, c.b); got != c.expected {
t.Errorf("Add(%d, %d) = %d, 期望 %d", c.a, c.b, got, c.expected)
}
})
}
}
测试返回 error 的函数(同样是测试文件,用 go test 运行):
package main
import (
"errors"
"testing"
)
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("除数不能为零")
}
return a / b, nil
}
func TestDivide(t *testing.T) {
if q, err := Divide(10, 4); err != nil || q != 2.5 {
t.Errorf("Divide(10, 4) = %v, %v; 期望 2.5, nil", q, err)
}
if _, err := Divide(1, 0); err == nil {
t.Error("除以零应当返回错误")
}
}
常见坑
- 测试文件名不对:必须叫 xxx_test.go 才会被 go test 发现;测试函数必须是 TestXxx(t *testing.T) 的精确签名,写成 testAdd 或小写开头会被静默跳过。
- 用 t.Fatal 和 t.Error 不分:Error 标记失败但继续执行本测试后续代码,Fatal 立即终止本测试。前置条件不成立(如初始化失败)用 Fatal,普通断言用 Error。
- 测试之间共享状态:一个测试改了全局变量或临时文件不还原,后面的测试就会莫名失败且顺序相关。每个测试自造数据、自行清理(t.Cleanup 是好帮手)。
- 断言里打印一堆值却不说期望:t.Errorf("结果是 %v", got) 没有期望值,失败时要人工推理。永远写成「got X, want Y」的对照格式。
- 只在 happy path 写测试:错误分支、边界值(0、负数、空切片、超长输入)才是 bug 的藏身处。表驱动让补这些用例的边际成本几乎为零。
小结
测试文件 _test.go、函数 TestXxx、断言就 if 加 Errorf;表驱动加 t.Run 组织用例;go test -v/-race/-cover 是日常三件套。下一节认识 Go 1.18 的重磅特性:泛型。