登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go 怎么用 httptest.NewServer 测试重定向链

来源:17golang原创

时间:2026-09-07 13:02:49 240浏览 收藏

测试 HTTP 重定向时,最容易漏掉的不是 302 状态码,而是客户端到底访问了哪几跳。用 httptest.NewServer 搭一个只存在于测试期间的本地服务,再用 http.Client.CheckRedirect 记录目标请求,就能把“是否跟随”和“跟随到哪里”分开断言。

默认客户端会继续跟随重定向;想测试完整链路,就在服务端记录路径或在 CheckRedirect 中记录 req。想停在当前 3xx,则返回 http.ErrUseLastResponse,并主动关闭返回的响应体。
要点速览
  • httptest.NewServer 适合构造稳定、可关闭的重定向测试入口。
  • 服务端路径记录能验证真实到达链路,CheckRedirect 能验证客户端准备跟随的下一跳。
  • ErrUseLastResponse 会返回当前响应,不能把它当作普通错误处理。

先把每一跳写成可观察的测试服务器

先定义三段路径:/start 跳到 /middle/middle 再跳到 /final,最后返回 200。测试只关心重定向结构,所以不需要启动真实端口或依赖外部站点;NewServer 会返回带随机本地地址的测试服务器,结束时用 Close 释放它。

func newRedirectServer(t *testing.T, seen *[]string) *httptest.Server {
    t.Helper()
    return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        *seen = append(*seen, r.URL.Path) // 记录真实到达的每一跳
        switch r.URL.Path {
        case "/start":
            http.Redirect(w, r, "/middle", http.StatusFound) // 第一跳
        case "/middle":
            http.Redirect(w, r, "/final", http.StatusTemporaryRedirect) // 第二跳
        case "/final":
            w.WriteHeader(http.StatusOK)
            _, _ = w.Write([]byte("done")) // 终点只返回一个可断言结果
        default:
            http.NotFound(w, r)
        }
    }))
}

这里混用 302 和 307,是为了让测试明确知道每个节点返回什么;如果只验证 GET,二者都可以被客户端继续处理。seen 保存的是服务器实际收到的路径,适合断言最终链路,而不是猜测客户端行为。

Go httptest.NewServer 重定向测试中客户端、三个路径处理器、Location 响应头和响应体的静态关系
图1:在测试边界内查看客户端与三个路径处理器的关系,重定向通过 Location 连接到下一目标。

默认客户端如何断言完整重定向链

服务器准备好后,直接使用 ts.Client() 发起请求。这个客户端复用了测试服务器的传输配置;默认的 CheckRedirect 允许客户端继续跟随,直到最终响应或连续重定向达到上限。

func TestRedirectChain(t *testing.T) {
    var seen []string
    ts := newRedirectServer(t, &seen)
    defer ts.Close() // 测试结束关闭监听器和连接

    client := ts.Client()
    resp, err := client.Get(ts.URL + "/start") // 默认跟随重定向
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        t.Fatalf("最终状态码 = %d, want %d", resp.StatusCode, http.StatusOK)
    }
    if got, want := string(mustRead(t, resp.Body)), "done"; got != want {
        t.Fatalf("最终响应体 = %q, want %q", got, want)
    }
    wantPaths := []string{"/start", "/middle", "/final"}
    if !reflect.DeepEqual(seen, wantPaths) {
        t.Fatalf("到达路径 = %#v, want %#v", seen, wantPaths)
    }
}

示例中的 mustRead 只是把 io.ReadAll 的错误转成测试失败;实际文件中要补上 ioreflect 的导入。这个断言组合了最终状态、响应体和完整路径,任何一跳丢失都会暴露出来。生产代码若使用 POST,还要额外断言 301/302/303 与 307/308 对方法和请求体的不同语义。

用 CheckRedirect 观察客户端准备发出的请求

服务端记录回答“请求实际到了哪里”,而 CheckRedirect 回调回答“客户端准备跟随到哪里”。回调参数里的 req 是即将发出的请求,via 按最早到最近保存已经发出的请求。两者一起记录,定位错误跳转会更直接。

func TestRedirectTargets(t *testing.T) {
    var seen []string
    ts := newRedirectServer(t, &seen)
    defer ts.Close()

    var targets []string
    client := ts.Client()
    client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
        targets = append(targets, req.URL.Path) // 记录下一跳,不是当前响应路径
        if len(via) > 2 {
            return fmt.Errorf("重定向次数异常: %d", len(via)) // 提前暴露循环
        }
        return nil
    }
    resp, err := client.Get(ts.URL + "/start")
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close()

    want := []string{"/middle", "/final"}
    if !reflect.DeepEqual(targets, want) {
        t.Fatalf("下一跳 = %#v, want %#v", targets, want)
    }
}

回调不会在初始请求前执行,所以 targets/middle 开始;初始的 /start 仍由服务端 seen 记录。若回调返回普通错误,客户端会停止并返回一个包装后的 *url.Error,这适合把不允许的跨域或过长链路变成明确失败。

禁止跟随时怎么检查每一跳

有些单元测试并不想访问终点,只想检查当前响应的 StatusCodeLocation。这时返回 http.ErrUseLastResponse,客户端会把最近一份响应交给调用方。它与普通 CheckRedirect 错误不同:这里得到的是当前响应和 nil error,因此要按响应路径读取并关闭 Body。

func TestStopAtFirstRedirect(t *testing.T) {
    ts := newRedirectServer(t, &[]string{})
    defer ts.Close()

    client := ts.Client()
    client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
        return http.ErrUseLastResponse // 保留当前 302,不请求 /middle
    }
    resp, err := client.Get(ts.URL + "/start")
    if err != nil {
        t.Fatal(err)
    }
    defer resp.Body.Close() // ErrUseLastResponse 返回的 Body 不会替你关闭

    if resp.StatusCode != http.StatusFound {
        t.Fatalf("状态码 = %d, want %d", resp.StatusCode, http.StatusFound)
    }
    if got, want := resp.Header.Get("Location"), "/middle"; got != want {
        t.Fatalf("Location = %q, want %q", got, want)
    }
}

测试重定向时可以按下面的边界选择断言点:

目标观察位置关键断言
真实访问链Handlerseen 的路径顺序
下一跳策略CheckRedirectreq.URLvia 长度
停止当前跳返回值与响应ErrUseLastResponse、3xx、Location
资源清理调用方所有非 nil resp.Body 都关闭
Go http.Client CheckRedirect 重定向策略中目标请求、via 请求链、302 响应和 ErrUseLastResponse 的静态关系
图2:把客户端重定向策略与当前 302 响应分开观察,ErrUseLastResponse 让测试停在这一跳。

常见问题

为什么只断言最终 200 不够?

因为错误的中间跳转也可能最终到达 200。记录 Handler 路径,才能发现多跳、漏跳或意外访问了不该访问的路径。

CheckRedirect 里的 req 是当前请求还是下一请求?

它是客户端准备发出的下一请求;已经发出的请求按时间顺序放在 via 中,所以初始入口不会出现在回调记录里。

返回 ErrUseLastResponse 后为什么还要 Close Body?

这是保留当前响应的特殊分支,响应体交给调用方管理。无论最终是 200 还是 302,都应在读取后关闭 resp.Body

一条可靠的重定向测试通常同时保留两种视角:用 Handler 记录真实路径,用 CheckRedirect 检查客户端策略;需要停在某一跳时,再用 ErrUseLastResponse 固定断言边界。

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