当前位置:首页 > 文章列表 > Golang > Go教程 > Go 怎么用 httptest.NewServer 测试重定向链

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

来源:17golang原创 2026-09-07 13:02:49 0浏览 收藏

测试 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删除
Python Decimal 和 float 混用为什么会报类型错误Python Decimal 和 float 混用为什么会报类型错误
上一篇
Python Decimal 和 float 混用为什么会报类型错误
Linux ext4 文件删除后空间没有释放是什么原因
下一篇
Linux ext4 文件删除后空间没有释放是什么原因
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • SuperCLUE中文大模型评测基准:功能、能力维度与应用指南
    SuperCLUE
    SuperCLUE是权威的中文大语言模型综合评测基准,涵盖语言理解、知识应用、AI Agent智能体及安全性等12项核心能力。通过多轮对话与客观测试,定期发布榜单与技术报告,为模型研发、优化及行业选型提供科学依据。
    172次使用
  • C-Eval中文评测基准:大语言模型多学科能力评估指南
    C-Eval
    深入了解C-Eval中文评估套件,涵盖52个学科与4级难度。本文详解其功能特点、Zero-shot/Few-shot使用方法及代码示例,助您全面评测LLM中文理解与泛化能力。
    102次使用
  • AI Prompt Library:免费AI提示词库,助力ChatGPT高效创作与营销
    AI Prompt Library
    探索AI Prompt Library免费资源库,涵盖营销、写作及多场景AI提示词。兼容ChatGPT、Claude等工具,一键复制优化输出,提升工作效率。
    24次使用
  • LangGPT提示词框架:结构化Prompt设计方法与开源工具指南
    LangGPT
    LangGPT是一种受编程语言启发的结构化提示词设计工具,提供双层框架、模块化模板及变量功能,帮助用户高效编写高质量Prompt。该项目已在GitHub免费开源,适用于内容创作、编程辅助等多场景。
    35次使用
  • ClickPrompt:AI提示词生成与优化工具,支持Stable Diffusion、ChatGPT及代码辅助
    ClickPrompt
    ClickPrompt是一款专为AI提示词编写者设计的开源在线工具,支持Stable Diffusion绘图、ChatGPT对话及GitHub Copilot代码辅助。提供Prompt自动生成、一键运行、社区分享及可视化优化功能,帮助用户高效获取精准AI输出。
    74次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码