TestMain 是 Go 测试框架中唯一支持全局 setup/teardown 的入口函数,签名必须为 func TestMain(*testing.M),须调用 m.Run() 并返回其结果,setup/teardown 需置于 m.Run() 前后且 teardown 必须 defer 或确保执行;它按包生效,不跨包共享,不可调用 t.Log/t.Fatal,不可依赖 init 顺序,不可在未完成异步任务时直接 m.Run()。

TestMain 是什么,为什么不用它会出问题
TestMain 是 Go 测试框架提供的唯一能做全局 setup/teardown 的入口。不通过它初始化(比如启动 mock 服务、建测试 DB、设环境变量),而是在每个 TestXxx 里重复操作,会导致:测试间状态污染、DB 连接泄漏、端口被占、go test -race 报竞态——尤其当你跑 go test -v ./... 时,多个包并行执行,问题更隐蔽。
怎么写一个安全的 TestMain 函数
必须满足三个硬性条件,缺一不可:
- 函数签名严格为
func TestMain(m *testing.M) - 必须调用
m.Run()并返回其结果(不能自己 return 0) - 所有 setup/teardown 逻辑必须放在
m.Run()前后,且 teardown 要用defer或确保执行(哪怕 panic)
错误示例:return 0 会跳过测试执行;漏掉 defer cleanup() 会让资源残留;在 m.Run() 后没处理 panic,可能导致 teardown 被跳过。
func TestMain(m *testing.M) {
setupDB()
defer cleanupDB() // 即使 test panic 也会执行
os.Setenv("ENV", "test")
defer os.Unsetenv("ENV")
code := m.Run() // 必须调用,且用它的返回值
os.Exit(code)
}
多个测试文件共享同一套初始化,要注意什么
Go 的 TestMain 是 per-package 的:同一个包下所有 *_test.go 文件共用一个 TestMain。但如果你拆了多个包(如 pkg/db 和 pkg/api),它们各自需要独立的 TestMain —— Go 不会跨包合并或继承。
立即学习“go语言免费学习笔记(深入)”;
- 别把
TestMain写在main_test.go里就以为能管全项目;它只对当前go test所在目录(即包路径)生效 - 如果子包也要初始化,得在各自包目录下定义自己的
TestMain - 跨包复用初始化逻辑?抽成函数,但不要复用
TestMain函数本身(Go 不允许包内多个TestMain)
TestMain 里不能做的事
它不是普通函数,运行时机和上下文受限:
- 不能调用
t.Log()或t.Fatal()—— 因为没有*testing.T实例;想输出日志用fmt.Println或log.Println - 不能依赖
init()顺序做初始化 ——TestMain在所有init()之后、测试开始前执行,但不同包的init()顺序不确定 - 别在里面起 goroutine 等异步任务然后直接
m.Run()—— 没等它结束测试就跑了,容易出现 “connection refused” 类错误
常见现象:TestMain 启动了一个本地 HTTP server,但没等它 listen 成功就进了 m.Run(),结果所有测试都连不上 localhost:8080。
真正麻烦的是:TestMain 的生命周期和测试进程强绑定,一旦写错,问题往往不报错,只是行为飘忽 —— 比如有时测试通过、有时失败,或者只在 CI 上崩。盯住 m.Run() 的调用位置和 cleanup 的执行保障,比写功能代码还容易翻车。










