Hurl 请求链实战:在单个 Hurl 文件中串联多个请求并用 JSONPath 断言 REST API
【免费下载链接】hurlHurl, run and test HTTP requests with plain text.项目地址: https://gitcode.com/GitHub_Trending/hu/hurl
本篇指南基于 Hurl 官方教程中的 "Chaining Requests" 章节,演示如何在同一个.hurl文件中按顺序组织多个 HTTP 请求:从追加第二个请求开始,逐步扩展到测试返回 JSON 的 REST API、使用[Query]参数段、应用类型化谓词与count/regex过滤器。读完后,你将能够编写一个包含多个 entry 的完整 Hurl 测试文件,理解hurl --test的执行语义,并掌握对 JSON 响应做精确断言的各种写法。
教程上下文:从 basic.hurl 继续
本教程承接前两步(你的第一个 Hurl 文件 与 添加断言)。在开始请求链之前,basic.hurl里只有一个请求,已经对首页 HTML 与响应头做了大量断言:
# Checking our home page: GET http://localhost:3000 HTTP 200 [Asserts] xpath "string(//head/title)" == "Movies Box" xpath "//h3" count == 2 xpath "string((//h3)[1])" contains "Popular" xpath "string((//h3)[2])" contains "Featured Today" # Testing HTTP response headers: header "Content-Type" == "text/html; charset=utf-8" cookie "x-session-id" exists cookie "x-session-id[HttpOnly]" exists此时只有一个 HTTP 请求,但响应上已经积累了大量测试。教程强调的原则是:断言写得越多,测试套件就越不易脆弱。接下来要把其他请求也加进来,继续增加测试。
追加第二个请求:同一文件中的多个 entry
在同一个文件里,直接在第一请求之后写第二个请求即可。这里验证目标:对坏链接,服务器应返回 404 页面(404 是 HTTP 标准的 "Not Found" 状态码,语义可参考 MDN 文档)。
修改后的basic.hurl:
# Checking our home page: GET http://localhost:3000 HTTP 200 [Asserts] xpath "string(//head/title)" == "Movies Box" xpath "//h3" count == 2 xpath "string((//h3)[1])" contains "Popular" xpath "string((//h3)[2])" contains "Featured Today" # Testing HTTP response headers: header "Content-Type" == "text/html; charset=utf-8" cookie "x-session-id" exists cookie "x-session-id[HttpOnly]" exists # Check that we have a 404 response for broken links: GET http://localhost:3000/not-found HTTP 404 [Asserts] header "Content-Type" == "text/html; charset=utf-8" xpath "string(//h2)" == "Error" xpath "string(//h3)" == "Not Found"现在文件里有两个entry:每个 entry 由一个请求和一个期望响应描述(expected response description)组成。两个请求按文件顺序依次执行,每个响应都可以被独立测试。
响应描述是可选的。文件也可以只写请求:
GET http://localhost:3000 GET http://localhost:3000/not-found但这种写法几乎等于不做测试。不过这类文件在另一种场景下很有用:当你把 Hurl 当作取数工具(用纯文本发请求、拿回响应内容)而非测试工具时。
运行验证:
$ hurl --test basic.hurl basic.hurl: Success (2 request(s) in 21 ms) -------------------------------------------------------------------------------- Executed files: 1 Executed requests: 2 (90.9/s) Succeeded files: 1 (100.0%) Failed files: 0 (0.0%) Duration: 22 ms测试仍然通过,此时已经是两个请求按序执行。
--test模式的执行语义
从 手册 对--test选项的说明可以确认几个影响链式测试行为的细节:
--test激活测试模式:HTTP 响应体不再打印到标准输出,而是逐个 Hurl 文件报告进度,全部跑完后输出一段文本汇总(即上面看到的Executed files / Succeeded files表格);- 测试模式下,多个文件默认并行执行(每个文件一个工作线程、互不共享状态);要按文件顺序串行执行,加
--jobs 1; - 该选项对应环境变量
HURL_TEST,属于仅命令行可用的选项。
注意区分两个层面:文件与文件之间在测试模式下可并行,而单个文件内部的多个请求始终是顺序执行的——后一个请求可以用前一个请求捕获到的值(教程后续章节的 Captures 会用到这一特性)。
测试 REST API:JSONPath 断言
到目前为止测试的是两个 HTML 端点。下面测试 REST API:示例网站在http://localhost:3000/api/health暴露了一个 health 资源。
先在 shell 里直接验证。Hurl 作为经典 CLI 应用,可以像很多 Unix 工具一样从标准输入读取请求(用-或管道),并把结果管道给其他工具(如jq):
$ echo 'GET http://localhost:3000/api/health' | hurl {"status":"RUNNING","healthy":true,"operationId":6212054377712155,"reportedDate":"2023-07-21T16:11:24.053Z"} $ echo 'GET http://localhost:3000/api/health' | hurl | jq { "status": "RUNNING", "healthy": true, "operationId": 8629192252836205, "reportedDate": "2023-08-04T11:04:52.516Z" }这意味着 Hurl 既能写文件化、可版本化的回归测试,也能当交互式探针使用——排查问题时先echo ... | hurl看一眼真实响应,再把请求固化进.hurl文件加断言。
把 health API 加进basic.hurl,用JSONPath 断言:
# Check our health API: GET http://localhost:3000/api/health HTTP 200 [Asserts] header "Content-Type" == "application/json; charset=utf-8" jsonpath "$.status" == "RUNNING" jsonpath "$.healthy" == true jsonpath "$.operationId" existsJSONPath 断言与 XPath 断言结构相同:一个查询(JSONPath 表达式,用于检查 JSON 对象)+ 一个谓词。与 XPath 断言一样,JSONPath 谓词的值是带类型的:字符串、布尔、数字、日期和集合都受支持(见 JSONPath 断言)。
从源码结构看,请求执行结果与 JSONPath 断言的求值/比较逻辑集中在运行器中,例如 packages/hurl/src/runner/result.rs 里包含JsonPathAssert相关处理;断言语义本身的规范则以 docs/spec 下的说明为准。
从浏览器 XHR 到 API 断言
教程的第二个例子来自 Movies Box 网站的用户功能:可以按演员、导演、上映年份搜索电影,搜索页在http://localhost:3000/search。输入 "1982" 就能看到 1982 年上映的电影。搜索页通过浏览器的 XHR 请求后端 REST APIhttp://localhost:3000/api/search获取结果——用浏览器开发者工具可以看到这条请求:
把这个"页面上看到的请求"直接变成 Hurl 断言,就是对 API 做回归测试的典型路径:
# Check search API: GET http://localhost:3000/api/search?q=1982&sort=name HTTP 200 [Asserts] header "Content-Type" == "application/json; charset=utf-8" jsonpath "$" count == 5 jsonpath "$[0].name" == "Blade Runner" jsonpath "$[0].director" == "Ridley Scott" jsonpath "$[0].release_date" == "1982-06-25"教程提示:Movies Box 为了教学方便直接内置了 mock 数据,生产应用不应这样做。更稳妥的做法是通过环境变量为应用提供 "integration/debug" 模式(或 mock 数据库),让断言在已知数据上运行。
这里用到了count过滤器:jsonpath "$" count == 5断言整个结果数组有 5 个元素(count统计集合元素个数,见 filters 文档)。
使用 [Query] 参数段代替 URL 拼接
上面的 URL 直接写了查询参数?q=1982&sort=name。Hurl 也支持把它们放进请求的[Query]段:
# Check search API: GET http://localhost:3000/api/search [Query] q: 1982 sort: name HTTP 200 [Asserts] header "Content-Type" == "application/json; charset=utf-8" jsonpath "$" count == 5 jsonpath "$[0].name" == "Blade Runner" jsonpath "$[0].director" == "Ridley Scott" jsonpath "$[0].release_date" == "1982-06-25"关于[Query]段,请求语法文档 给出了几条需要记住的规则:
- 查询参数由
字段: 值组成,段以[Query]开头; - 参数段中的值不做 URL 编码(这与直接写在 URL 里的参数不同,URL 里需要手工转义,如
Install%20Linux),因此含空格、特殊字符的值用参数段写起来更直观; - 如果 URL 和
[Query]段同时存在参数,最终请求会合并两者一起发送,而不是相互覆盖; - 各请求段(
[Cookies]、[Query]、[Form]等)之间无固定顺序,可以任意混合书写,但请求体必须位于请求的最后。
谓词与过滤器:从"等于"到"格式校验"
到这里只测试了"服务端返回值等于期望值"。Hurl 的断言体系由三层组成:查询(xpath/jsonpath/header/cookie 等)、过滤器(对查询结果做变换)、谓词(对变换后的值做判断)。
谓词(predicates)
除==与exists外,断言文档的谓词表 定义了更多类型化谓词:
| 谓词 | 语义 | 示例 |
|---|---|---|
startsWith | 查询结果以谓词值开头(字符串或二进制) | jsonpath "$.movie" startsWith "The" |
endsWith | 查询结果以谓词值结尾(字符串或二进制) | jsonpath "$.movie" endsWith "Back" |
contains | 集合包含该值;或字符串/二进制包含该子串 | jsonpath "$.numbers" contains 42 |
matches | 查询字符串部分匹配正则模式 | jsonpath "$.release" matches /\d{4}/ |
任何谓词都可以通过前缀not取反(如not contains)。类型约束要注意:startsWith/contains只能作用于字符串和字节,matches只能作用于字符串;查询结果是数字时不能套用字符串谓词。
把release_date的断言从精确相等放宽为前缀匹配:
# Check search API: GET http://localhost:3000/api/search [Query] q: 1982 sort: name HTTP 200 [Asserts] header "Content-Type" == "application/json; charset=utf-8" jsonpath "$" count == 5 jsonpath "$[0].name" == "Blade Runner" jsonpath "$[0].director" == "Ridley Scott" jsonpath "$[0].release_date" startsWith "1982"startsWith "1982"只验证年份开头。如果想既宽松又严格——只关心年份、但要求日期整体符合YYYY-MM-DD格式——就引入过滤器。
regex 过滤器:从查询值中提取片段
filters 文档 中的regex过滤器:提取正则的捕获组内容,模式至少需要一组捕获组;正则语法遵循 Rustregexcrate(Hurl 用 Rust 编写,正则实现与regexcrate 的语法一致)。
最终写法:
# Check search API: GET http://localhost:3000/api/search [Query] q: 1982 sort: name HTTP 200 [Asserts] header "Content-Type" == "application/json; charset=utf-8" jsonpath "$" count == 5 jsonpath "$[0].name" == "Blade Runner" jsonpath "$[0].director" == "Ridley Scott" jsonpath "$[0].release_date" regex /(\d{4})-\d{2}-\d{2}/ == "1982"逐段拆解这条断言:
jsonpath "$[0].release_date"—— JSONPath 查询,从响应中提取第一部电影的上映日期;regex /(\d{4})-\d{2}-\d{2}/—— regex 过滤器,正则写法与 JavaScript 一样用/.../包裹;其中捕获组(\d{4})把 4 位年份从完整日期中提取出来;== "1982"—— 谓词,断言提取出的年份等于期望值。
这条断言同时完成了两件事:格式校验(不匹配YYYY-MM-DD就失败)+ 值校验(年份必须是 1982)。过滤器可以级联组合来细化查询值,这是 Hurl 断言表达力的核心来源——前面已经用过一次count(jsonpath "$" count == 5),这里是第二次使用过滤器。
完整的四请求测试文件与运行结果
最终,包含四个 HTTP 请求的basic.hurl完整形态:
# Checking our home page: GET http://localhost:3000 HTTP 200 [Asserts] xpath "string(//head/title)" == "Movies Box" xpath "//h3" count == 2 xpath "string((//h3)[1])" contains "Popular" xpath "string((//h3)[2])" contains "Featured Today" # Testing HTTP response headers: header "Content-Type" == "text/html; charset=utf-8" cookie "x-session-id" exists cookie "x-session-id[HttpOnly]" exists # Check that we have a 404 response for broken links: GET http://localhost:3000/not-found HTTP 404 [Asserts] header "Content-Type" == "text/html; charset=utf-8" xpath "string(//h2)" == "Error" xpath "string(//h3)" == "Not Found" # Check our health API: GET http://localhost:3000/api/health HTTP 200 [Asserts] header "Content-Type" == "application/json; charset=utf-8" jsonpath "$.status" == "RUNNING" jsonpath "$.healthy" == true jsonpath "$.operationId" exists # Check search API: GET http://localhost:3000/api/search [Query] q: 1982 sort: name HTTP 200 [Asserts] header "Content-Type" == "application/json; charset=utf-8" jsonpath "$" count == 5 jsonpath "$[0].name" == "Blade Runner" jsonpath "$[0].director" == "Ridley Scott" jsonpath "$[0].release_date" regex /(\d{4})-\d{2}-\d{2}/ == "1982"运行:
$ hurl --test basic.hurl basic.hurl: Success (4 request(s) in 21 ms) -------------------------------------------------------------------------------- Executed files: 1 Executed requests: 4 (181.8/s) Succeeded files: 1 (100.0%) Failed files: 0 (0.0%) Duration: 22 ms每个请求的所有断言全部成功,Failed files为 0。
小结
- Hurl 文件由多个entry组成,entry 之间用空行分隔,按顺序执行;同一文件里可以混合 HTML 页面测试与 REST API 测试,每个响应独立断言;
- 响应描述(
HTTP 200、[Asserts]等)是可选的,纯请求文件适合把 Hurl 当取数 CLI 用(可配echo ... | hurl | jq管道); - JSONPath 断言 = 查询 + 谓词,谓词值带类型;
startsWith/endsWith/contains/matches等谓词都可用not取反; - 查询参数优先用
[Query]段书写(值不做 URL 编码,与 URL 内参数合并发送); - 过滤器(
count、regex等)可与查询、谓词自由组合,实现"提取 + 变换 + 判断"三级断言。
教程的收尾建议同样值得记住:随着 Hurl 文件增长,不要吝啬写注释——Hurl 文件本身就会成为应用一份"可执行的文档"(executable documentation),它既是测试,也是随时可运行的接口说明。下一章 调试技巧 会讲解断言失败时如何快速定位问题。
【免费下载链接】hurlHurl, run and test HTTP requests with plain text.项目地址: https://gitcode.com/GitHub_Trending/hu/hurl
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考