测试逻辑复用的本质是提取可组合的纯断言函数与显式状态准备,采用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 user %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调用的函数里设,否则错误行号会指向工具函数内部而非测试用例
避免用 struct 封装测试上下文,除非状态强耦合
有人倾向定义 type UserTestSuite struct { DB *sql.DB; t *testing.T } 并挂方法,但这容易导致:1)误用 t 导致并发 panic(*testing.T 不是线程安全的);2)忘记在每个方法开头调用 t.Helper();3)难以隔离测试间状态。只有当多个测试必须共享昂贵初始化(如启动 mock HTTP server + 清理钩子),才考虑用 struct,且必须确保每个方法接收独立的 *testing.T。
- 如果只是复用断言逻辑,函数比 struct 更轻量、更易测试、更难出错
- 若需共享 setup/teardown,优先用
t.Cleanup()配合普通函数,而非 struct 生命周期管理 - 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子测试名用输入值,便于快速定位失败 case - 不要在循环里 defer 清理资源(如文件句柄),defer 会累积到外层函数结束才执行,可能造成泄漏
assertUserCreated(t, id, "alice") 返回的 error 如果只写 "name mismatch",调试时就得翻源码看哪一行调用的——务必把关键变量(id、expectedName)塞进 error message。










