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 保存的是服务器实际收到的路径,适合断言最终链路,而不是猜测客户端行为。

默认客户端如何断言完整重定向链
服务器准备好后,直接使用 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 的错误转成测试失败;实际文件中要补上 io 和 reflect 的导入。这个断言组合了最终状态、响应体和完整路径,任何一跳丢失都会暴露出来。生产代码若使用 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,这适合把不允许的跨域或过长链路变成明确失败。
禁止跟随时怎么检查每一跳
有些单元测试并不想访问终点,只想检查当前响应的 StatusCode 和 Location。这时返回 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)
}
}
测试重定向时可以按下面的边界选择断言点:
| 目标 | 观察位置 | 关键断言 |
|---|---|---|
| 真实访问链 | Handler | seen 的路径顺序 |
| 下一跳策略 | CheckRedirect | req.URL 与 via 长度 |
| 停止当前跳 | 返回值与响应 | ErrUseLastResponse、3xx、Location |
| 资源清理 | 调用方 | 所有非 nil resp.Body 都关闭 |

常见问题
为什么只断言最终 200 不够?
因为错误的中间跳转也可能最终到达 200。记录 Handler 路径,才能发现多跳、漏跳或意外访问了不该访问的路径。
CheckRedirect 里的 req 是当前请求还是下一请求?
它是客户端准备发出的下一请求;已经发出的请求按时间顺序放在 via 中,所以初始入口不会出现在回调记录里。
返回 ErrUseLastResponse 后为什么还要 Close Body?
这是保留当前响应的特殊分支,响应体交给调用方管理。无论最终是 200 还是 302,都应在读取后关闭 resp.Body。
一条可靠的重定向测试通常同时保留两种视角:用 Handler 记录真实路径,用 CheckRedirect 检查客户端策略;需要停在某一跳时,再用 ErrUseLastResponse 固定断言边界。
-
441 收藏
-
Golang · Go教程 | 29分钟前 | HTTP · go · sse · 实时通信 · 流式响应 · Go EventSource SSE Server-Sent Events http.Flusher ResponseController491 收藏
-
367 收藏
-
482 收藏
-
292 收藏
-
133 收藏
-
123 收藏
-
381 收藏
-
110 收藏
-
462 收藏
-
419 收藏
-
212 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习