首页 >  Golang >  Go教程

Go httptest.ResponseRecorder 怎样准确读取 Handler 的最终状态码

来源:17golang原创

时间:2026-10-12 16:14:56 415浏览 收藏

我在给一个 HTTP Handler 补单元测试时,第一次踩到的坑不是断言写错,而是读错了对象:直接看 ResponseRecorder.Code,有些“什么也没写”的分支会得到 0;太早调用 Result(),拿到的又不是 Handler 完成后的响应快照。稳定的写法是让 Handler 先完整返回,再用 Result().StatusCode 断言最终结果;只有需要确认是否显式调用过 WriteHeader 时,才单独观察 Code。

官方文档:https://pkg.go.dev/net/http/httptest#ResponseRecorder

要点速览
  • Code记录写入路径,可能保留 0;Result().StatusCode提供隐式 200。
  • Result()必须在 Handler 完成后调用,返回的是响应头和 Body 的快照。
  • 状态码只认第一次写入,测试应覆盖显式状态、首次 Write 和完全无写入三种情况。

先分清 Code 与 Result().StatusCode 的职责

ResponseRecorder 是一个记录 http.ResponseWriter 变化的测试对象。Code更像“记录器内部当前记下的状态”,而 Result()把这些记录整理为可断言的 http.Response。两者通常都会得到 200,却不是同一个观察层。

读取位置适合回答的问题边界
rec.CodeHandler 是否显式写入了状态码,记录器当前保存什么Handler 没有调用 WriteHeader 或 Write 时可能为 0
rec.Result().StatusCode最终响应应按哪个 HTTP 状态码断言必须等 Handler 运行结束;结果对象是快照
rec.HeaderMap历史兼容代码读取内部字段已标记弃用,测试应从 Result().Header 读取响应头
Go ResponseRecorder 中 Handler、WriteHeader、Code、Result 和 StatusCode 的静态关系说明图
图1:ResponseRecorder 写入记录与结果快照的静态说明图,不是运行截图。

把状态码读取放到 Handler 完成之后

我现在会把“调用 Handler”和“读取结果”分成两个明显阶段。下面的测试不启动端口,只验证 Handler 直接写入 Recorder 的行为:

package handler_test

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestHandlerStatus(t *testing.T) {
    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 先写出明确的状态,后续断言才能验证首次写入规则。
        w.WriteHeader(http.StatusCreated)
    })

    req := httptest.NewRequest(http.MethodGet, "http://example.com/items", nil)
    rec := httptest.NewRecorder()
    handler.ServeHTTP(rec, req)

    // Handler 已经返回,再读取最终响应快照并关闭 Body。
    resp := rec.Result()
    defer resp.Body.Close()
    if resp.StatusCode != http.StatusCreated {
        t.Fatalf("status = %d, want %d", resp.StatusCode, http.StatusCreated)
    }
}

关键不是 NewRecorder 这一行,而是调用顺序。官方文档明确要求 Result() 只能在 Handler 结束后调用,因为它会生成响应头快照,并把 Body 包装成可读取的响应体。把它放进 Handler 仍在执行的 goroutine 中,会让断言和真实完成时机脱钩。

覆盖 204、重复 WriteHeader 与隐式 200

ResponseRecorder 遵循 ResponseWriter 的核心规则:第一次写状态后,后续 WriteHeader 不会覆盖它;直接调用 Write 会隐式提交 200。完全没有调用 WriteHeader 或 Write 时,Code可能仍为 0,但 Result().StatusCode会按隐式成功状态返回 200。

func TestRecorderBoundaries(t *testing.T) {
    cases := []struct {
        name string
        handler http.Handler
        want int
    }{
        {
            name: "no write uses implicit ok in Result",
            handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                // 故意不写 Header 和 Body,检查 Result 的默认状态。
            }),
            want: http.StatusOK,
        },
        {
            name: "body write commits ok",
            handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                // 首次 Write 会提交 200;此处不再手动调用 WriteHeader。
                _, _ = w.Write([]byte("ready"))
            }),
            want: http.StatusOK,
        },
        {
            name: "first status wins",
            handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                // 第二次状态不会覆盖第一次,测试应固定这个边界。
                w.WriteHeader(http.StatusNoContent)
                w.WriteHeader(http.StatusInternalServerError)
            }),
            want: http.StatusNoContent,
        },
    }

    for _, tc := range cases {
        t.Run(tc.name, func(t *testing.T) {
            rec := httptest.NewRecorder()
            tc.handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
            // 只在调用返回后读取 Result,避免观察中间状态。
            resp := rec.Result()
            defer resp.Body.Close()
            if resp.StatusCode != tc.want {
                t.Fatalf("status = %d, want %d", resp.StatusCode, tc.want)
            }
        })
    }
}
Go ResponseRecorder 显式 204、首次 Write 和隐式 200 的状态断言关系说明图
图2:显式状态、隐式 200 与首次写入规则的静态关系图,不是运行截图。

这里的 204 还要注意 Body 边界:204 不允许响应体,测试重点是状态码和必要响应头,不要把“写入 Body 的返回值”当成状态成功的唯一证据。若测试想检查响应头,优先使用 resp.Header,不要依赖已经弃用的 HeaderMap。

回归测试中的检查清单

  1. 先构造 httptest.NewRecorder(),再调用 Handler;不要在 Handler 尚未返回时读取结果。
  2. 断言最终 HTTP 语义时使用 rec.Result().StatusCode;只有排查显式写入路径时才看 rec.Code。
  3. 覆盖“无写入”“直接 Write”“先 WriteHeader 后重复写入”三类分支,尤其保留 204、304 等无 Body 状态。
  4. 读完 resp.Body 后关闭它,响应头从 resp.Header 读取。

我自己的取舍是:业务 Handler 的黑盒状态断言统一走 Result(),底层响应包装器的白盒测试才检查 Code。这样既不会把内部实现字段当公开语义,也能在需要时定位“到底是谁第一次提交了状态”。

常见问题

为什么 ResponseRecorder.Code 会是 0?

当 Handler 既没有调用 WriteHeader,也没有调用 Write 时,记录器可能保留 0;要断言 HTTP 默认成功,请读取 Result().StatusCode。

Result() 可以在 Handler 内部调用吗?

不建议也不符合契约。它必须在 Handler 完成后调用,否则响应头和 Body 可能还没有形成最终快照。

重复调用 WriteHeader 为什么没有得到最后一个状态?

HTTP 响应只接受第一次提交的状态,后续调用不会覆盖已写入的状态;测试应明确断言第一次状态。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>