单元测试第一原则是隔离外部依赖,必须mock数据库、http调用等真实系统,使用testify/mock或gomock替换接口实现,避免ci失败、随机超时等问题。

单元测试必须隔离外部依赖
Go微服务里写单元测试,第一原则不是“测得全”,而是“不碰真实外部系统”。数据库连接、HTTP调用、消息队列发消息——这些在单元测试里全得 mock 或 stub 掉。
常见错误现象:go test 本地跑得通,CI 里失败;测试偶尔超时或随机失败;修改一个函数导致十几个测试报错。
- 用
testify/mock或gomock替换接口实现,比如把UserService依赖的DBClient换成 mock 实例 - 对 HTTP 客户端,别直接 new
http.Client,封装一层可注入的 transport,测试时用httptest.NewServer或httpmock - 避免在
TestXxx函数里调用os.Setenv或改全局变量——它会污染其他并行测试,加t.Parallel()前务必清理 - 表驱动测试(table-driven)是 Go 单元测试事实标准:每个 case 独立 setup/teardown,错误定位快,新增分支逻辑只需加一行结构体
集成测试要启动真实依赖容器
集成测试不是“多跑几个函数”,而是验证服务之间真实协作是否成立。比如订单创建后,barista 服务是否真的收到了 OrderCreated 事件,状态是否更新到 DB。
关键不是自己手搭 PostgreSQL 或 Kafka,而是用 testcontainers-go 启动轻量级容器实例——它能自动拉镜像、暴露端口、等就绪、自动清理。
-
testcontainers-go启动 PostgreSQL 容器后,拿到动态端口和连接字符串,传给被测服务的NewService构造函数 - 不要在
TestMain里全局初始化 DB 连接池——每个测试应有自己的干净 schema,用pgx的Exec执行CREATE SCHEMA IF NOT EXISTS test_XXX - 事件驱动场景下,集成测试需显式等待:比如用
time.AfterFunc+ channel 超时控制,或轮询 DB 查状态变更,避免time.Sleep(2 * time.Second)这种不可靠写法 - CI 中集成测试应加
//go:build integration标签,本地开发默认跳过,用go test -tags=integration显式触发
race 检测必须在 CI 中强制开启
Go 微服务天然并发密集,但数据竞争几乎不会在功能测试里暴露。90% 以上的竞态问题,只会在 go test -race 下浮出水面——而这个 flag 一旦开,运行时间翻倍,所以很多人本地关掉、CI 也漏掉。
-
-race不是可选项,是上线前必过门槛。它检测的是 goroutine 间非同步读写同一内存地址,比如 map 并发读写、struct 字段无锁修改 - 若测试因
-race报错,别绕过,要定位到具体哪行——race report 里会标出 read at / write at 的 stack trace,通常就是漏了sync.RWMutex或 channel 同步点 - 第三方库若本身有 race(如某些老版本
gopkg.in/yaml.v2),升级或换gopkg.in/yaml.v3,别 suppress - 本地开发可加 alias:
alias gotest='go test -race -v',养成肌肉记忆
测试覆盖率不能只看数字
go test -cover 显示 85%,不代表业务主路径安全。微服务里真正危险的是边界条件:空请求体、重复事件、数据库唯一约束冲突、超时重试后的幂等性。
- 覆盖指标只盯三类代码:错误分支(
if err != nil后的处理)、状态机转移(如 order status 从pending→confirmed→shipped)、外部依赖返回异常(mock 返回context.DeadlineExceeded) - 别为 getter/setter、DTO struct、日志打印行写测试——它们没逻辑,加了反而拖慢反馈周期
- 用
go tool cover -func=coverage.out查具体函数未覆盖行,比总百分比更有用;重点关注 handler、service、domain 层,infra 层(如 db migration)可忽略 - 集成测试不计入覆盖率统计——它测的是组合行为,不是单个函数行执行,强行塞进 cover profile 反而模糊焦点
集成测试最易被忽略的点,是环境一致性:本地 docker-compose up 跑的 Kafka 版本,和 CI 里 testcontainers 拉的镜像 tag 必须一致,否则序列化格式、协议版本差异会导致“本地通、CI挂”这类问题。











