Testcontainers-go 是最稳妥的真实依赖集成测试方案,通过 Docker API 启动轻量容器并绑定生命周期,需动态获取端口、添加健康检查、用 Wire 构建独立测试依赖图、跨服务调用加超时重试、按 schema 隔离数据库数据。
集成测试不是 mock 所有外部依赖,而是让被测服务连接真实的数据库、Redis、Kafka 等。硬编码本地端口或要求开发者预装服务,会导致 CI 失败、环境不一致。testcontainers-go 是目前最稳妥的选择——它通过 Docker API 启动轻量容器,生命周期绑定到 Go 测试函数。

TestMain 或每个 TestXxx 开头调用 testcontainers.RunContainer,并用 defer container.Terminate(ctx) 清理container.MappedPort 动态获取,不能写死 5432
PING Redis 或执行 SELECT 1),否则服务可能因依赖未就绪而启动失败docker://docker:dind)或使用 setup-docker actionctx := context.Background()
req := testcontainers.ContainerRequest{
Image: "redis:7-alpine",
ExposedPorts: []string{"6379/tcp"},
}
redisC, _ := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
port, _ := redisC.MappedPort(ctx, "6379")
redisAddr := fmt.Sprintf("localhost:%s", port.Port())
// 接着初始化你的 service 实例,传入 redisAddr
微服务通常用 wire 管理依赖注入。集成测试时,你不能沿用生产 Wire Set——比如生产用 redis.NewClient,测试却要连 Docker Redis;数据库连接字符串也完全不同。关键是在测试包里定义独立的 WireSet,显式替换具体实现。
main.go 的 InitializeApp 里硬编码初始化逻辑,所有 newXXX 函数应提取为变量或接口app/testwire/wire.go,引入 testcontainer 启动的实例,并注入到 Service 构造中bufconn 模拟 server,但仅限于单元级;跨服务集成仍需真实 server 容器两个服务都跑起来了,不代表它们能立刻通信。网络就绪、gRPC server 启动完成、HTTP 路由注册完毕,都有延迟。直接发请求然后断言响应,大概率遇到 connection refused 或空响应。
http.DefaultClient.Do + time.Sleep 不可靠,改用 retry.Do(如 github.com/avast/retry-go)封装请求WithBlock() 和短超时(如 500ms),避免阻塞整个测试;连接失败时捕获 status.Code(err) == codes.Unavailable
http.StatusCode 或 gRPC status.Code,再解析 JSON/Protobuf——很多失败源于状态码非 200 却强行解码time.Sleep(3 * time.Second)),它既拖慢测试,又无法覆盖慢机器场景多个测试并发运行时,共用一个 PostgreSQL 容器极易相互污染:A 测试插入用户,B 测试删掉同名用户,结果 A 断言失败。靠 “每次测试前后 truncate 所有表” 效率低、易漏表、且破坏外键约束。
test_12345),测试结束时 DROP SCHEMA ... CASCADE
gorm.Config.NamingStrategy 的 TablePrefix 设为 schema 名,避免改模型定义db.AutoMigrate 在测试中反复建表——它不处理字段删除、类型变更,容易导致后续测试查不到字段真实微服务集成测试最难的不是启动容器,而是让服务之间“等得恰到好处、清得干干净净、断得明明白白”。任何一步省略超时控制、忽略错误码、跳过 cleanup,都会让测试从“发现问题”退化成“随机失败”。