讲解

测试在 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 的重磅特性:泛型。