测试逻辑复用的本质是提取可组合的纯断言函数与显式状态准备,采用func(*testing.T, ...any) error形式,由调用方决定错误处理方式,避免全局状态和t.Helper()误用。
Go 测试中不能像其他语言那样直接继承 TestCase 或用装饰器包装测试函数,所以复用必须靠函数封装 + 显式调用。核心不是“让多个测试跑同一段代码”,而是“让每个测试能精准控制输入、观察输出、验证行为”。这意味着:复用单元必须是纯函数(无全局状态)、接受明确参数、返回可判定结果(如 error 或布尔值)。
func(t *testing.T, args ...any) error 形式定义工具函数这是最稳妥、最符合 Go testing 约定的方式。工具函数不自己调用 t.Fatal,而是把错误交还给调用方处理——这样上层测试可以决定是失败(t.Fatal)、跳过(t.Skip)还是仅记录(t.Log)。
func assertUserCreated(t *testing.T, userID string, expectedName string) error {
user, err := db.FindUserByID(userID)
if err != nil {
return fmt.Errorf("failed to fetch use
r %s: %w", userID, err)
}
if user.Name != expectedName {
return fmt.Errorf("user.Name = %q, want %q", user.Name, expectedName)
}
return nil
}
func TestCreateUser(t *testing.T) {
id := createUserInDB(t, "alice")
if err := assertUserCreated(t, id, "alice"); err != nil {
t.Fatal(err)
}
}
assert 或 require 开头,语义清晰t.Helper() —— 它只应在直接被 TestXxx 调用的函数里设,否则错误行号会指向工具函数内部而非测试用例有人倾向定义 type UserTestSuite struct { DB *sql.DB; t *testing.T } 并挂方法,但这容易导致:1)误用 t 导致并发 panic(*testing.T 不是线程安全的);2)忘记在每个方法开头调用 t.Helper();3)难以隔离测试间状态。只有当多个测试必须共享昂贵初始化(如启动 mock HTTP server + 清理钩子),才考虑用 struct,且必须确保每个方法接收独立的 *testing.T。
t.Cleanup() 配合普通函数,而非 struct 生命周期管理t.Fatal 会中断当前 goroutine,但不会终止整个测试包 —— 这点常被误解为“suite 失效”,其实是预期行为把测试数据和期望结果写成 slice,每轮循环调用相同的工具函数,既保持测试可读性,又避免重复粘贴断言块。
func TestValidateEmail(t *testing.T) {
tests := []struct {
input string
isValid bool
}{
{"a@b.c", true},
{"@", false},
{"", false},
}
for _, tt := range tests {
t.Run(tt.input, func(t *testing.T) {
err := assertValidEmail(t, tt.input)
if tt.isValid && err != nil {
t.Fatal(err)
}
if !tt.isValid && err == nil {
t.Fatal("expected validation to fail")
}
})
}
}
assertValidEmail 是一个纯断言工具函数,只负责验证并返回 error
t.Run 子测试名用输入值,便于快速定位失败 caseassertUserCreated(t, id, "alice") 返回的 error 如果只写 "name mismatch",调试时就得翻源码看哪一行调用的——务必把关键变量(id、expectedName)塞进 error message。