真正可靠的做法是将 HTTP 客户端作为参数显式传入函数,测试时注入自定义 client(如 httptest.Server),避免污染全局变量;对 SDK 则定义最小接口+适配器,数据库测试优先用 testcontainers-go 启临时 PostgreSQL。

Go 测试里怎么替换外部 HTTP 客户端
直接改 http.DefaultClient 不安全,测试之间会互相污染;用全局变量注入又难控制生命周期。真正可靠的做法是把客户端作为依赖显式传入,而不是在函数内部硬编码调用 http.Get 或 http.Client.Do。
比如你有个函数依赖外部 API:
func FetchUserInfo(client *http.Client, url string) (string, error) {
resp, err := client.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
return string(body), nil
}
测试时就可传入自定义的 http.Client,其 Transport 字段指向一个 mock.RoundTripper(比如用 net/http/httptest 的 Server,或轻量级的 gock)。
- 别在测试里改
http.DefaultClient.Transport—— 多个测试并行跑时会冲突 - 如果函数签名没暴露
*http.Client,先重构:这是注入 Mock 的前提,不是“为了测试而改代码”,而是让依赖显性化 - 用
httptest.NewServer比写RoundTripper实现更稳,尤其对状态码、header、重定向等边界场景
interface{} 类型的第三方 SDK 怎么 Mock
像 aws-sdk-go、redis-go 这类包,核心客户端类型往往不带导出接口,没法直接实现。强行用 monkey 打补丁或反射替换方法,测试脆弱、Go 1.21+ 后还可能 panic。
立即学习“go语言免费学习笔记(深入)”;
正确路径是「面向接口编程」:自己定义最小契约接口,再用适配器包装 SDK 客户端。
type S3Uploader interface {
PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
}
type awsS3Adapter struct {
client *s3.Client
}
func (a *awsS3Adapter) PutObject(...) { return a.client.PutObject(...) }
这样测试时只需 mock S3Uploader 接口,完全隔离 SDK。
- 别为整个 SDK 写大而全的 interface —— 只提取当前业务真正在用的方法
- 适配器类型(如
awsS3Adapter)放 production 包里,测试包只依赖 interface,不 import SDK - 如果 SDK 方法带大量可选参数(如
optFns),Mock 实现里留空函数体即可,不用模拟全部行为
数据库操作测试该用内存 SQLite 还是真实 PostgreSQL
用 :memory: 的 SQLite 快,但 SQL 行为和 PostgreSQL 差异明显:比如 jsonb、ON CONFLICT、序列生成、锁粒度。一旦上线就翻车。
本地开发用 Docker 起一个临时 PostgreSQL 容器,测试完自动销毁,才是贴近真实的方案。Golang 生态有 testcontainers-go 封装得足够轻量。
- 别在
TestMain里手动exec.Command("docker run")—— 端口冲突、清理失败、Windows 兼容性全是坑 -
testcontainers-go的GenericContainer配置里记得设WaitingFor: wait.ForListeningPort("5432/tcp"),否则连接可能提前失败 - 每个测试用独立 database name(通过
CREATE DATABASE test_123),避免事务隔离干扰
gomock 生成的 Mock 对象为什么总 panic
常见原因是调用未预期的方法,或者期望调用次数没 match 上。gomock 默认 strict 模式,只要实际调用和 EXPECT() 不完全一致就 panic,不是 bug,是设计如此。
典型触发点:忘记调 mockCtrl.Finish(),或在同一个 EXPECT() 上重复调用但没声明 .AnyTimes() 或 .Times(n)。
- 所有
gomock.Controller必须在测试末尾调mockCtrl.Finish(),建议用defer mockCtrl.Finish() - 如果被测函数内部可能多次调同一方法,必须显式写
.Times(3)或.AnyTimes(),不能只写一次EXPECT() - 参数匹配尽量用
gomock.Any()或gomega.BeEquivalentTo()(配合gomock.WantAllArgs()),少用具体值比对,避免因日志字段、时间戳等无关差异失败
Mock 是手段不是目的,真正难的是识别哪些依赖必须隔离、哪些可以接受集成。越早把“调外部服务”这个动作从逻辑里剥出来,后面测试和演进就越省力。










