集成测试核心是“可控地连通”,即仅模拟直接依赖(如gRPC),其余保持真实(如HTTP服务用httptest启动、Redis用miniredis、数据库用docker-compose),配合-test.short守卫、端口自动分配、健康检查与context超时控制,确保稳定可维护。

用 testify/mock 模拟依赖服务再启动真实 HTTP 服务
集成测试的核心不是“测得全”,而是“可控地连通”。Golang 里最常踩的坑是:把集成测试写成端到端黑盒,结果数据库没清空、外部 API 偶发超时、端口被占——失败原因和业务逻辑完全无关。
正确做法是:只对「当前服务直接依赖的下游」做模拟,其余保持真实。比如你的 HTTP 服务依赖一个用户中心 gRPC 接口和一个 Redis 缓存,那就用 testify/mock 实现 gRPC mock server,用 redis.MockServer(或 github.com/alicebob/miniredis/v2)起轻量 Redis 实例;但你自己的 HTTP handler 必须用 httptest.NewUnstartedServer 启动真实监听,再发请求验证组合行为。
- 别用
http.HandlerFunc包裹 handler 去测——那只是单元测试,绕过了路由、中间件、JSON 解码等真实链路 -
httptest.NewServer会自动分配空闲端口,但每次调用都新建 goroutine 和 listener,记得在defer srv.Close()清理 - mock 的 gRPC server 要确保返回的 error 类型与生产一致(比如
status.Error(codes.NotFound, ...)),否则中间件可能 panic
用 go test -run=TestIntegration 隔离集成测试用例
集成测试慢、不稳定、依赖环境,不能和单元测试混跑。Golang 官方不强制分类,但必须靠命名 + 标签控制执行流。
推荐统一前缀 TestIntegration*,并在函数开头加守卫:
立即学习“go语言免费学习笔记(深入)”;
func TestIntegrationOrderCreate(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
// ... real setup & assert
}这样就能用 go test -short ./... 快速过 CI 单元测试,而本地手动运行 go test -run=TestIntegration -timeout=60s 执行集成部分。
- 不要用
build tag(如//go:build integration)——它会让测试文件彻底不参与编译,丢失 import 检查和 IDE 跳转 - 避免在
init()或包级变量中初始化 DB/Redis 连接,否则所有测试(包括单元)都会触发连接,导致-short失效 - 超时设为 60s 而非默认 10s,因为真实网络 I/O、容器启动、SQL 执行都不可控
用 docker-compose up -d redis db 启动真实依赖而非全部 mock
有些逻辑绕不开真实存储行为:PostgreSQL 的事务隔离级别、JSONB 字段查询、Redis 的 EXPIRE 自动清理。这时候 mock 只会掩盖问题。
方案是:CI 和本地都用 docker-compose 起最小依赖集,测试代码通过环境变量控制连接地址(如 DB_HOST=postgres / DB_HOST=localhost:5432),并确保每次测试前重置状态:
- PostgreSQL:用
pg_dump --schema-only导出 DDL,测试前执行CREATE DATABASE testdb+psql -d testdb -f schema.sql - Redis:用
FLUSHDB(不是FLUSHALL)清空当前 DB,避免影响其他测试进程 - Docker Compose 文件里禁用
restart: always,防止容器异常存活干扰下次测试
服务组合时用 WaitGroup + context.WithTimeout 控制启动顺序与超时
当集成测试要拉起多个本地服务(如 auth-service、order-service、gateway),顺序和健康检查比单服务复杂得多。硬写 time.Sleep(2 * time.Second) 是反模式——慢机器上不够,快机器上又浪费时间。
标准做法是每个服务启动后暴露 /healthz 端点,主测试进程用 http.Get 轮询,配合 sync.WaitGroup 和 context.WithTimeout 控制整体等待窗口:
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
for _, svc := range []string{"http://localhost:8081/healthz", "http://localhost:8082/healthz"} {
if err := waitUntilReady(ctx, svc); err != nil {
t.Fatalf("service %s not ready: %v", svc, err)
}
}真正容易被忽略的是:每个服务子进程必须绑定同一个 context,一旦超时就主动退出,否则残留进程会卡住后续测试。










