用 httptest 可免启动真实服务测试 HTTP handler:NewRecorder 适合单元测试 handler 逻辑,NewServer 适合测试客户端;gomock 要基于 interface mock 依赖;中间件和时间/随机数需抽象为可注入依赖以保证测试稳定。

用 net/http/httptest 快速 mock HTTP handler 测试接口逻辑
不需要启动真实服务,httptest.NewServer 或 httptest.NewRecorder 就能验证路由、状态码、响应体。关键在于把 handler 当作普通函数调用,不依赖网络。
-
httptest.NewRecorder()用于捕获响应(ResponseWriter),适合单元测试 handler 函数本身 -
httptest.NewServer(http.Handler)启动临时服务,适合测试客户端代码(比如你写的http.Client调用) - 别直接传
nil给http.ServeHTTP—— 它会 panic;务必用httptest.NewRecorder()构造有效的http.ResponseWriter
func TestUserHandler(t *testing.T) {
req, _ := http.NewRequest("GET", "/user/123", nil)
rr := httptest.NewRecorder()
handler := http.HandlerFunc(userHandler) // 假设 userHandler 是你的 http.HandlerFunc
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusOK {
t.Errorf("expected status OK, got %d", rr.Code)
}
if !strings.Contains(rr.Body.String(), `"id":123`) {
t.Error("response body doesn't contain expected JSON")
}
}
用 gomock + mockgen 替换依赖的 interface(如数据库、第三方 SDK)
Go 没有运行时反射 mock,必须基于 interface。如果你的 handler 依赖了 UserService,那它必须是接口类型,且方法可被 gomock 生成 mock 实现。
- 不能对 struct 或具体类型 mock ——
mockgen只接受interface{}作为输入 - 生成命令示例:
mockgen -source=service.go -destination=mocks/mock_service.go -package=mocks - 使用时需手动调用
mockCtrl.Finish(),否则 expect 未满足会 panic(常见漏写点) - 避免在测试中 new 出真实 service 实例再赋值给 interface 字段——这等于没 mock
func TestCreateUserWithMockDB(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mocks.NewMockUserRepository(ctrl)
mockRepo.EXPECT().Create(gomock.Any()).Return(int64(99), nil)
svc := &UserService{repo: mockRepo}
_, err := svc.Create(&User{Name: "alice"})
if err != nil {
t.Fatal(err)
}
}
测试带中间件的 handler(如 JWT 验证、日志)时如何绕过或控制依赖行为
中间件通常包装原始 handler,测试时若不处理其依赖(如 jwt.Parse、log.Printf),会导致测试不可控或失败。
- 把外部依赖抽象成字段或选项参数,例如
type AuthMiddleware struct { Parser TokenParser },测试时注入返回固定user的 mockTokenParser - 不要在中间件里硬编码
os.Stdout或log.Default()—— 改为接收io.Writer或*log.Logger,测试时传io.Discard - 如果中间件依赖
http.Request.Context()中的值(如ctx.Value("user")),用req = req.WithContext(context.WithValue(req.Context(), key, value))注入
避免在测试中误用 time.Now() 或 rand.Intn() 导致结果不稳定
这类函数让测试变成「概率性通过」,尤其在并发测试或 CI 环境下容易偶发失败。
立即学习“go语言免费学习笔记(深入)”;
- 将时间/随机数生成器作为依赖传入,例如
type Clock interface { Now() time.Time },测试时用固定返回值的实现 - 不要在 handler 内直接调用
time.Now().Unix()—— 提取为可注入的方法或函数变量(如var nowFunc = time.Now),测试前替换 - 同理,
math/rand应封装为 interface 或接受*rand.Rand实例,避免全局rand.Seed影响其他测试
真正难的不是写 mock,而是设计出可测的接口边界——比如一个 handler 里混着解析 JSON、查 DB、发邮件、写日志,那就得先拆。










