必须用testing.m进行全局初始化和清理,因其能保证执行顺序、捕获测试状态,并支持flag解析与流程控制;init()和包级变量无法满足这些需求。

什么时候必须用 testing.M
当你需要在所有测试运行前做一次全局初始化(比如启动数据库、加载配置、设置环境变量),或在所有测试结束后做清理(关闭连接、删除临时文件),又或者想控制测试的执行流程(如跳过某些测试、提前退出),testing.M 就不是“可选”,而是唯一可靠的方式。单纯靠 init() 或包级变量初始化,无法保证顺序,也无法捕获测试失败状态。
TestMain 函数签名和基本结构
TestMain 必须是包内唯一的函数,签名严格为 func TestMain(m *testing.M),且必须调用 m.Run() —— 否则测试会卡住不结束。它替代了默认的测试主函数,因此你得自己负责调用 os.Exit() 传入 m.Run() 的返回值。
常见错误现象:go test 无输出、挂起、或报错 no tests to run,往往是因为忘了 os.Exit(m.Run()) 或写成了 return m.Run()(Go 不允许直接 return int 给 void 函数)。
正确写法示例:
func TestMain(m *testing.M) {
// 初始化:只执行一次
setupDatabase()
defer cleanupDatabase() // 注意:defer 在 os.Exit 前仍会执行
// 运行所有测试
code := m.Run()
// 清理:只执行一次,在所有测试之后
os.Exit(code)
}
避免在 TestMain 中做耗时或非幂等操作
TestMain 是单次调用,但它运行的上下文并不隔离:多个 go test 并发执行时(如 go test ./... -p=4),每个包独立运行自己的 TestMain;但同一包内,它不会重入。问题在于——如果你在里面启动了监听端口、写入固定路径的临时文件、或修改了全局状态(如 http.DefaultClient),就可能与其他测试冲突或导致 flaky 行为。
使用场景建议:
- 初始化只读配置(如从
config.json读取并存为包级常量) - 启动本地测试服务(如用
httptest.NewUnstartedServer,并绑定随机端口) - 设置
os.Setenv+defer os.Unsetenv(注意:子进程继承环境,但测试间不隔离)
别做这些:
- 直接
os.RemoveAll("/tmp/testdata")—— 改用os.MkdirTemp("", "test-*")每次新建 - 调用
log.SetOutput全局改日志输出 —— 改用局部log.New - 在
TestMain里跑go func() { ... }()然后没等完就m.Run()—— 并发需显式同步
与 TestXxx 和 init() 的执行顺序关系
执行顺序固定为:init() → TestMain → 所有 TestXxx(按字母序)→ TestMain 剩余代码(即 m.Run() 之后)。这意味着:
-
init()发生最早,但无法访问flag参数(如-test.v),也不知是否真要跑测试 -
TestMain可以解析flag,例如跳过集成测试:if !testing.Verbose() { os.Exit(0) } - 每个
TestXxx仍应保持独立,不能依赖TestMain中未同步的共享状态(比如一个未加锁的 map)
容易被忽略的一点:TestMain 的 defer 语句,只在 m.Run() 返回后才触发 —— 所以它适合放清理逻辑,但不适合放“测试前必须完成”的初始化(比如 DB 连接池必须在 m.Run() 前建好)。











