如何为Golang Gin框架的认证中间件编写单元测试
如何为Gin认证中间件编写单元测试
我来帮你搞定这个Gin中间件的单元测试!要验证中间件的两种认证场景(API Key/JWT)以及错误情况,我们需要用Gin的测试工具模拟请求,同时mock掉依赖的外部函数,确保测试不依赖真实的数据库或密钥服务。下面是具体的实现步骤和完整测试代码:
前置准备
首先确保你的依赖函数(parseAPIKey、findUserByAPIKey、parseToken、findUserByJWT)是包级可赋值变量(就像你初始代码里那样),这样我们才能在测试中替换它们的实现。另外,推荐使用github.com/stretchr/testify/assert来简化断言(用标准库testing也可以,只是testify更直观)。
核心测试思路
- 用Gin的
gin.CreateTestContext创建模拟的请求上下文 - 为每个测试场景mock对应的依赖函数
- 设置请求头(比如
X-API-Key或Authorization) - 调用中间件处理请求
- 验证响应状态码、错误信息,以及请求上下文是否携带用户ID
完整测试代码
辅助函数(用于从上下文取用户ID)
首先我们需要一个辅助函数来从请求上下文里提取用户ID,对应你中间件里的addUserIdToContext:
// 假设你的UserID上下文键是consts.ProjectContextKeys.UserIDCtxKey func getUserIdFromContext(r *http.Request) string { userId, ok := r.Context().Value(consts.ProjectContextKeys.UserIDCtxKey).(string) if !ok { return "" } return userId }
测试用例实现
import ( "net/http" "net/http/httptest" "testing" "github.com/gin-gonic/gin" "github.com/stretchr/testify/assert" ) func TestAuthMiddleware(t *testing.T) { // 初始化Gin为测试模式,避免输出冗余日志 gin.SetMode(gin.TestMode) // 场景1:API Key验证成功,上下文携带用户ID t.Run("valid api key should set user id in context", func(t *testing.T) { // Mock依赖函数 mockUserID := "user_123" parseAPIKey = func(c *gin.Context, sc *cfg.Server) (string, error) { return "valid_api_key", nil } findUserByAPIKey = func(apiKey string, o *orm.ORM) (*models.User, error) { return &models.User{ID: mockUserID}, nil } // 创建测试上下文 w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodGet, "/test", nil) c.Request.Header.Set("X-API-Key", "valid_api_key") // 设置正确的API Key // 调用中间件 svc := &cfg.Server{} middleware := Middleware("/test", svc, &orm.ORM{}) middleware(c) // 验证结果 assert.Equal(t, http.StatusOK, w.Code) // 中间件调用c.Next(),所以状态码是200(除非后续 handler 修改) assert.Equal(t, mockUserID, getUserIdFromContext(c.Request)) }) // 场景2:JWT Token验证成功,上下文携带用户ID t.Run("valid jwt token should set user id in context", func(t *testing.T) { // Mock依赖函数 mockUserID := "user_456" parseAPIKey = func(c *gin.Context, sc *cfg.Server) (string, error) { return "", ErrEmptyAPIKeyHeader // 触发JWT认证分支 } parseToken = func(c *gin.Context, sc *cfg.Server) (*jwt.Token, error) { // 返回一个包含有效claims的JWT Token claims := jwt.MapClaims{ "exp": 1735689600, // 未来的过期时间 "iss": "test_issuer", "jti": mockUserID, "sub": "test@example.com", } return &jwt.Token{Claims: claims, Method: jwt.SigningMethodHS256}, nil } findUserByJWT = func(email, provider, userID string, o *orm.ORM) (*models.User, error) { return &models.User{ID: mockUserID}, nil } // 创建测试上下文 w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodGet, "/test", nil) c.Request.Header.Set("Authorization", "Bearer valid_jwt_token") // 设置JWT头 // 调用中间件 svc := &cfg.Server{} middleware := Middleware("/test", svc, &orm.ORM{}) middleware(c) // 验证结果 assert.Equal(t, http.StatusOK, w.Code) assert.Equal(t, mockUserID, getUserIdFromContext(c.Request)) }) // 场景3:错误的API Key,返回403 Forbidden t.Run("invalid api key should return 403", func(t *testing.T) { // Mock依赖函数 parseAPIKey = func(c *gin.Context, sc *cfg.Server) (string, error) { return "invalid_api_key", nil } findUserByAPIKey = func(apiKey string, o *orm.ORM) (*models.User, error) { return nil, ErrForbidden // 模拟用户不存在 } // 创建测试上下文 w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodGet, "/test", nil) c.Request.Header.Set("X-API-Key", "invalid_api_key") // 调用中间件 svc := &cfg.Server{} middleware := Middleware("/test", svc, &orm.ORM{}) middleware(c) // 验证结果 assert.Equal(t, http.StatusForbidden, w.Code) // 如果你的authError返回了JSON错误信息,还可以验证响应体 // assert.JSONEq(t, `{"error":"forbidden"}`, w.Body.String()) }) // 场景4:无效的JWT Token(缺少exp字段),返回对应错误 t.Run("jwt missing exp field should return error", func(t *testing.T) { // Mock依赖函数 parseAPIKey = func(c *gin.Context, sc *cfg.Server) (string, error) { return "", ErrEmptyAPIKeyHeader } parseToken = func(c *gin.Context, sc *cfg.Server) (*jwt.Token, error) { // 返回一个缺少exp字段的claims claims := jwt.MapClaims{ "iss": "test_issuer", "jti": "user_789", "sub": "test@example.com", } return &jwt.Token{Claims: claims, Method: jwt.SigningMethodHS256}, nil } // 创建测试上下文 w := httptest.NewRecorder() c, _ := gin.CreateTestContext(w) c.Request = httptest.NewRequest(http.MethodGet, "/test", nil) c.Request.Header.Set("Authorization", "Bearer invalid_jwt_token") // 调用中间件 svc := &cfg.Server{} middleware := Middleware("/test", svc, &orm.ORM{}) middleware(c) // 验证结果 assert.Equal(t, http.StatusBadRequest, w.Code) // 假设你的ErrMissingExpField对应400 // 验证错误信息 // assert.JSONEq(t, `{"error":"missing exp field"}`, w.Body.String()) }) }
关键注意事项
- 测试模式:记得调用
gin.SetMode(gin.TestMode),避免Gin在测试时输出不必要的日志。 - Mock函数的重置:如果你的测试用例之间有依赖,记得在每个测试用例结束后重置mock函数,避免影响其他测试。比如可以用
t.Cleanup:t.Cleanup(func() { parseAPIKey = originalParseAPIKey // 保存原始函数,测试后恢复 findUserByAPIKey = originalFindUserByAPIKey }) - 响应体验证:如果你的
authError函数返回了结构化的JSON错误,一定要验证响应体的内容,确保返回的错误信息正确。 - 上下文键的正确性:确保你从上下文提取用户ID时用的键和中间件里
addUserIdToContext用的键完全一致。
内容的提问来源于stack exchange,提问作者rak1n
相关产品推荐
相关产品推荐

