You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为Golang Gin框架的认证中间件编写单元测试

如何为Gin认证中间件编写单元测试

我来帮你搞定这个Gin中间件的单元测试!要验证中间件的两种认证场景(API Key/JWT)以及错误情况,我们需要用Gin的测试工具模拟请求,同时mock掉依赖的外部函数,确保测试不依赖真实的数据库或密钥服务。下面是具体的实现步骤和完整测试代码:

前置准备

首先确保你的依赖函数(parseAPIKey、findUserByAPIKey、parseToken、findUserByJWT)是包级可赋值变量(就像你初始代码里那样),这样我们才能在测试中替换它们的实现。另外,推荐使用github.com/stretchr/testify/assert来简化断言(用标准库testing也可以,只是testify更直观)。

核心测试思路

  1. 用Gin的gin.CreateTestContext创建模拟的请求上下文
  2. 为每个测试场景mock对应的依赖函数
  3. 设置请求头(比如X-API-Key或Authorization)
  4. 调用中间件处理请求
  5. 验证响应状态码、错误信息,以及请求上下文是否携带用户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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.01 01:57:48