测试 http handler 应用 httptest.newrequest 和 httptest.newrecorder 模拟真实请求流,确保中间件、路由参数等生效;dao 层优先使用内存实现接口进行单元测试,避免依赖真实数据库;并发测试需显式构造 goroutine 竞态场景。

测试 HTTP handler 时别直接调用函数
Go 的 http.HandlerFunc 不是普通函数,它依赖 http.ResponseWriter 和 *http.Request 上下文。直接调用 handler 函数(比如 handler(w, r))看似简单,但会绕过 net/http 的请求生命周期,导致中间件、路由参数、body 解析等逻辑失效,测试结果失真。
正确做法是用 httptest.NewRequest 和 httptest.NewRecorder 构造真实请求流:
req := httptest.NewRequest("GET", "/users/123", nil)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
这样能捕获状态码、响应头、body 内容,也兼容你实际使用的中间件链。
- 注意:若 handler 依赖 URL 查询参数或路径变量(如
chi或gorilla/mux),需在req上设置对应字段(req.URL.RawQuery或通过 router 的ServeHTTP调用) - 避免在测试中硬编码 JSON 字符串校验 body;用
json.Unmarshal解析后比对结构体字段更可靠 -
rr.Body.String()在多次读取时可能为空——Body是bytes.Buffer,读完即清空;需要重复使用时先rr.Body.Bytes()缓存
数据库层测试优先用内存实现而非真实 DB
单元测试不该依赖外部服务,尤其是 PostgreSQL/MySQL。启动容器、建库、清理表不仅慢(单测从毫秒级变成秒级),还引入 flaky 风险(网络超时、权限错误、端口冲突)。
推荐方案是为 DAO 层定义接口(如 UserRepository),测试时注入内存实现:
type InMemoryUserRepo struct {
users map[string]*User
}
这种实现可快速 setup/teardown,且完全可控。若必须测 SQL 语句逻辑(如复杂 JOIN 或 pgx 扫描行为),再单独跑集成测试,用 testcontainers-go 启一个临时 Postgres 实例。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 不要用
sqlmock模拟database/sql——它只验证调用顺序和参数,不校验 SQL 语法或类型映射是否正确,容易掩盖 real DB 下的 panic - 内存 repo 中的 map key 建议用主键字段(如
id),而非整条 struct;否则查找、更新逻辑易出错且难 debug - 如果业务强依赖数据库约束(如唯一索引、外键),内存实现无法覆盖,这类场景应归入集成测试范围
并发测试要主动触发竞态,而不是靠运气
Go 单元测试默认不开启 race detector,而微服务常涉及共享状态(如缓存、计数器、全局配置)。仅靠单 goroutine 测试根本发现不了 data race。
必须显式构造并发访问:
var wg sync.WaitGroup for i := 0; i <p>然后用 <code>go test -race</code> 运行。注意:race 检测本身有开销,仅在 CI 或本地 debug 时启用,不要放进常规 test 命令。</p>
- 并发测试里避免用
time.Sleep等待——它不可靠且拖慢执行;改用sync.WaitGroup或chan struct{}同步 - 若被测代码含
time.After或context.WithTimeout,测试中应注入可控制的 clock(如github.com/benbjohnson/clock),否则 timeout 时间不可控 - 并发写 map 会 panic,但 race detector 可能漏报——务必确保所有 map 访问都加锁或用
sync.Map
Mock 外部依赖时警惕“过度 mock”
用 gomock 或 mockery 生成接口 mock 很方便,但容易陷入“为 mock 而 mock”。比如对一个只返回固定 JSON 的第三方 HTTP client,与其 mock 整个 Do 方法,不如直接替换 http.Client.Transport 为 RoundTripFunc:
client := &http.Client{
Transport: RoundTripFunc(func(req *http.Request) (*http.Response, error) {
return &http.Response{
StatusCode: 200,
Body: io.NopCloser(strings.NewReader(`{"id":1}`)),
}, nil
}),
}
这种方式轻量、直观,且不依赖代码生成工具。只有当依赖逻辑复杂(如需校验多次调用顺序、参数组合)时,才值得上完整 mock。
- mock 的方法名和参数应与真实接口严格一致;否则编译通过但运行 panic,尤其注意指针接收者 vs 值接收者
- 避免在 mock 中做耗时操作(如 sleep、文件 IO)——这会让测试变慢且难以定位瓶颈
- 每个测试 case 应只 mock 必需的最小接口集合;mock 过多说明被测模块职责太重,该拆分
真正难的不是写更多断言,而是判断哪些逻辑值得测试、哪些该交给集成或 e2e 覆盖。比如 JWT token 签发/校验逻辑,测签名算法本身意义不大,重点应是你的 service 是否正确解析 claims 并拒绝非法 token。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










