Go单元测试需满足文件名以_test.go结尾、函数名以Test开头、参数为*testing.T;go test默认只运行当前目录测试,-run支持正则匹配;应避免log.Fatal/os.Exit,改用t.Fatal/t.Error;推荐表驱动测试与接口抽象解耦外部依赖。
go test 运行最简单元测试Go 的单元测试不需要额外框架,只要文件名以 _test.go 结尾、函数名以 Test 开头、参数为 *testing.T,就能被 go test 自动识别。不满足任一条件,测试就不会执行。
go test 默认只运行当前目录下的 *_test.go 文件,不会递归子目录TestAdd 合法,testAdd 会被忽略go test -run=TestAdd,-run 后跟的是正则匹配名,不是函数全名testing.T 的常见误用:别在测试里用 log.Fatal 或 os.Exit
测试中调用 log.Fatal、panic 或 os.Exit(1) 会导致整个测试进程退出,后续测试全部中断,且 go test 会报 exit status 1,掩盖真实失败原因。
t.Fatal 或 t.Errorf + t.FailNow(),它们只终止当前测试函数,不影响其他测试t.Error 记录错误但继续执行,适合检查多个断言;t.Fatal 遇错即停,适合前置条件校验(如初始化失败)os.Exit(比如 CLI 工具),需通过接口抽象或构建可替换的 os.Exit 函数变量来解耦,否则无法测试Go 社区推荐用结构体切片定义测试用例,避免大量复制粘贴的 TestXxx 函数。关键在于把输入、期望输出、描述聚合成一个数据项,再用循环统一执行断言。
func TestParseDuration(t *testing.T) {
cases := []struct {
name string
input string
expected time.Duration
wantErr bool
}{
{"zero", "0s", 0, false},
{"seconds", "30s", 30 * time.Second, false},
{"invalid", "1y", 0, true},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := time.ParseDuration(tc.input)
if tc.wantErr {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != tc.expected {
t.Errorf("ParseDuration(%q) = %v, want %v", tc.input, got, tc.expected)
}
})
}
}t.Run 创建子测试,让每个用例独立计时、独立失败,输出带名字(如 --- FAIL: TestParseDuration/seconds)name 必须有,否则子测试名是空字符串,调试困难tc 变量做闭包 —— Go 中循环变量复用,所有 goroutine 或延迟函数会看到最后一个值;必须用 tc := tc 显式拷贝测试不应该依赖数据库、HTTP 服务或文件系统。真实 I/O 不仅慢、不稳定,还会让测试变成集成测试。Go 推崇“依赖倒置”:把具体实现抽成接口,测试时传入内存实现。
http.DefaultClient,而是定义 type HTTPDoer interface { Do(*http.Request) (*http
.Response, error) }
Do 方法返回预设响应,完全绕过网络*sql.DB)也一样:定义 Querier 接口,只暴露 QueryRow 等方法,测试时用内存 map 模拟结果gomock),多数场景纯 Go 就够用;mock 库增加编译依赖和学习成本,且容易过度模拟测试最难的不是写断言,而是识别哪些逻辑该被隔离、哪些状态需要重置。比如全局变量、单例、时间相关代码(time.Now())、随机数生成器(rand.Intn)——这些都得通过参数注入或接口抽象才能可控。没做这一步,测试就只是“能跑”,不是“可信”。