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

Go请求处理器中Context值添加的单元测试问题排查

Go请求处理器Context单元测试问题解析

问题背景

编写了一个Go请求处理器函数,会在请求Context中添加键值对,但单元测试时从原请求Context获取handler添加的AuditFields时,出现interface conversion: interface {} is nil, not logrus.Fields的panic。需要明确为何Context未回传,以及如何正确测试请求中的Context值。

测试代码

for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        body := strings.NewReader(tt.payload)

        ctx := context.Background()
        ctx = context.WithValue(ctx, http.ContextUserKey, http.Account{ID: accID.String()})
        req, _ := nethttp.NewRequest("POST", "/api/groups/", body)
        req = req.WithContext(ctx)
        req.Header.Add("Content-Type", tt.contentType)

        rr := httptest.NewRecorder()

        router := mux.NewRouter()
        router.HandleFunc("/api/groups/", s.HandlePostGroups(kc, es, mySQL)).Methods("POST")
        router.ServeHTTP(rr, req)

        if rr.Code != tt.status {
            t.Errorf("wrong status code: got %v want %v", rr.Code, tt.status)
        }
        auditFields := req.Context().Value(http.AuditFields).(logrus.Fields)
        // Audit fields is what I want to assert. and are added in the request handler function
    })
}

处理器相关代码

func (s *IDMServer) HandlePostGroups(kc keycloak.API, es auth.KeyCreator, mySQL store.IDMRelationalStore) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx := r.Context()
        // Many things happening here but somewhere in code I do this
        if err != nil {
            s.Log.WithField("error", err).Error("Unable to create key")
            auditFields["event.outcome"] = "failure"
            r = r.WithContext(context.WithValue(ctx, server.AuditFields, auditFields))
            return
        }
    }
}

问题原因

  • Context不可变性与请求副本隔离:Go的context.Context是不可变类型,调用WithValue会生成新的Context实例。处理器中执行r = r.WithContext(...)只是修改了handler内部的请求副本,测试代码中访问的还是最初创建的req对象,其Context完全没被修改过,自然拿不到新增的AuditFields。
  • 分支未触发+无类型检查:只有当err != nil的分支执行时,处理器才会添加AuditFields。如果测试用例没触发该分支,原Context中不存在这个键,直接强制类型断言就会触发panic。

解决方法

方法一:通过包装handler捕获更新后的请求

在测试中包装目标handler,保存handler处理后的请求对象,从这个对象的Context中取值:

for _, tt := range tests {
    t.Run(tt.name, func(t *testing.T) {
        body := strings.NewReader(tt.payload)

        ctx := context.Background()
        ctx = context.WithValue(ctx, http.ContextUserKey, http.Account{ID: accID.String()})
        req, _ := nethttp.NewRequest("POST", "/api/groups/", body)
        req = req.WithContext(ctx)
        req.Header.Add("Content-Type", tt.contentType)

        rr := httptest.NewRecorder()
        var capturedReq *http.Request

        router := mux.NewRouter()
        // 包装handler,保存处理后的请求
        router.HandleFunc("/api/groups/", func(w http.ResponseWriter, r *http.Request) {
            s.HandlePostGroups(kc, es, mySQL)(w, r)
            capturedReq = r
        }).Methods("POST")
        router.ServeHTTP(rr, req)

        if rr.Code != tt.status {
            t.Errorf("wrong status code: got %v want %v", rr.Code, tt.status)
        }

        // 从捕获的请求中取值,先做类型检查
        if capturedReq == nil {
            t.Fatal("no request captured")
        }
        auditFields, ok := capturedReq.Context().Value(server.AuditFields).(logrus.Fields)
        if !ok {
            t.Fatal("AuditFields not found or type mismatch")
        }
        // 断言AuditFields的内容,比如结果状态
        if auditFields["event.outcome"] != tt.expectedOutcome {
            t.Errorf("wrong outcome: got %v want %v", auditFields["event.outcome"], tt.expectedOutcome)
        }
    })
}

方法二:确保测试触发目标分支+安全取值

构造能触发err != nil的测试场景(比如模拟依赖返回错误、传入无效参数),同时在取值前先做类型检查避免panic:

// 测试用例中构造错误场景,比如让es.KeyCreator返回错误
tests := []struct {
    name         string
    payload      string
    contentType  string
    status       int
    expectedOutcome string
}{
    {
        name:         "create key failed",
        payload:      "invalid payload",
        contentType:  "application/json",
        status:       http.StatusInternalServerError,
        expectedOutcome: "failure",
    },
}

// 取值时先判断类型
auditFields, ok := req.Context().Value(http.AuditFields).(logrus.Fields)
if !ok {
    t.Fatal("AuditFields not present in context")
}

方法三:重构处理器解耦Context逻辑

将添加AuditFields的逻辑抽成独立函数,或者让处理器返回AuditFields(如果业务允许),避免依赖Context传递断言数据:

// 修改处理器返回AuditFields
func (s *IDMServer) HandlePostGroups(kc keycloak.API, es auth.KeyCreator, mySQL store.IDMRelationalStore) func(w http.ResponseWriter, r *http.Request) logrus.Fields {
    return func(w http.ResponseWriter, r *http.Request) logrus.Fields {
        ctx := r.Context()
        auditFields := logrus.Fields{}
        if err != nil {
            s.Log.WithField("error", err).Error("Unable to create key")
            auditFields["event.outcome"] = "failure"
            r = r.WithContext(context.WithValue(ctx, server.AuditFields, auditFields))
            w.WriteHeader(http.StatusInternalServerError)
            return auditFields
        }
        // 其他逻辑
        auditFields["event.outcome"] = "success"
        return auditFields
    }
}

// 测试时直接获取返回值断言
handler := s.HandlePostGroups(kc, es, mySQL)
auditFields := handler(rr, req)
if auditFields["event.outcome"] != tt.expectedOutcome {
    t.Errorf("wrong outcome: got %v want %v", auditFields["event.outcome"], tt.expectedOutcome)
}

内容的提问来源于stack exchange,提问作者Apostolos

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 03:14:55