用 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 实现。
mockgen 只接受 interface{} 作为输入mockgen -source=service.go -destination=mocks/mock_service.go -package=mocks
mockCtrl.Finish(),否则 expect 未满足会 panic(常见漏写点)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.Parse、log.Printf),会导致测试不可控或失败。
type AuthMiddleware struct { Parser TokenParser },测试时注入返回固定 user 的 mock TokenParser
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 环境下容易偶发失败。
type Clock interface { Now() time.Time },测试时用固定返回值的实现time.Now().Unix() —— 提取为可注入的方法或函数变量(如 var nowFunc = time.Now),测试前替换math/rand 应封装为 interface 或接受 *rand.Rand 实例,避免全局 rand.Seed 影响其他测试真正难的不是写 mock,而是设计出可测的接口边界——比如一个 handler 里混着解析 JSON、查 DB、发邮件、写日志,那就得先拆。