Hurl: 纯文本 HTTP 请求测试工具¶
为什么要学 Hurl¶
API 测试有两个极端:一端是 Postman 这样的 GUI 工具(重、难以自动化),另一端是 curl 命令(轻、但难以组织和断言)。Hurl 在中间找到了平衡点:用纯文本文件描述 HTTP 请求和断言,像写测试用例一样测试 API。
| 特性 | curl | Postman | Hurl |
|---|---|---|---|
| 格式 | 命令行参数 | JSON (GUI) | 纯文本文件 |
| 断言能力 | 无 | 脚本 | 内置声明式断言 |
| CI/CD 友好 | 一般 | 需要 CLI | 原生 CLI |
| 学习曲线 | 低 | 中 | 低 |
| 变量支持 | 无 | 有 | 有 |
| 请求链 | 脚本 | 脚本 | 原生支持 |
| 捕获响应值 | 手动解析 | 脚本 | 内置 Capture |
| 重试机制 | 无 | 无 | 内置 |
Hurl 的 .hurl 文件格式直观到几乎不需要学习:看起来就像带注释的 HTTP 请求。
核心概念¶
白话解释¶
Hurl 文件就是一系列 HTTP 请求的"剧本"。每个请求包含三部分: 1. 请求:HTTP 方法 + URL + 可选的 Header/Body 2. 响应断言:验证响应状态码、Header、Body 3. 值捕获:从响应中提取值供后续请求使用
一个 .hurl 文件可以包含多个请求,按顺序执行,就像一个测试用例。
核心概念表¶
| 概念 | 说明 | 示例 |
|---|---|---|
| Entry | 一个请求+响应对 | GET https://api.com/users |
| Assert | 断言响应满足条件 | status == 200 |
| Capture | 从响应中提取值 | token: jsonpath "$.token" |
| Variable | 可复用的变量 | {{baseUrl}} |
| Filter | 值转换/处理 | jsonpath "$.name" == "Alice" |
| Section | 请求的组成部分 | [Headers], [QueryStringParams] |
| Option | 请求级别的配置 | [Options] retry: 3 |
Hurl 文件格式一览¶
# 这是一个完整的 Hurl 文件示例
# ---- 第一个请求:登录 ----
POST {{baseUrl}}/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "secret"
}
# 响应断言
HTTP 200
[Captures]
auth_token: jsonpath "$.token"
[Asserts]
jsonpath "$.token" exists
jsonpath "$.user.email" == "user@example.com"
# ---- 第二个请求:用 token 获取数据 ----
GET {{baseUrl}}/api/profile
Authorization: Bearer {{auth_token}}
HTTP 200
[Asserts]
jsonpath "$.name" isString
安装配置¶
安装方式¶
macOS
Linux
# Ubuntu/Debian
curl -LO https://github.com/Orange-OpenSource/hurl/releases/latest/download/hurl_amd64.deb
sudo dpkg -i hurl_amd64.deb
# Arch Linux
pacman -S hurl
# 通过 Cargo
cargo install hurl
Windows
验证安装¶
编辑器支持¶
- VS Code:安装 "Hurl" 扩展(语法高亮 + 运行)
- JetBrains:安装 "Hurl" 插件
- Vim/Neovim:tree-sitter 支持 hurl 语法
快速上手¶
第一个 Hurl 文件¶
运行:
带断言的请求¶
# test-api.hurl
# 测试 GET 请求
GET https://httpbin.org/get
[QueryStringParams]
name: Alice
age: 30
HTTP 200
[Asserts]
header "Content-Type" contains "application/json"
jsonpath "$.args.name" == "Alice"
jsonpath "$.args.age" == "30"
POST 请求¶
# 发送 JSON
POST https://httpbin.org/post
Content-Type: application/json
{
"name": "测试用户",
"email": "test@example.com"
}
HTTP 200
[Asserts]
jsonpath "$.json.name" == "测试用户"
# 发送表单
POST https://httpbin.org/post
[FormParams]
username: admin
password: secret
HTTP 200
常用命令行选项¶
# 运行 Hurl 文件
hurl test.hurl
# 显示详细信息
hurl --verbose test.hurl
# 非常详细(包含请求/响应 body)
hurl --very-verbose test.hurl
# 只显示测试结果
hurl --test test.hurl
# 传入变量
hurl --variable baseUrl=http://localhost:3000 test.hurl
# 设置超时
hurl --connect-timeout 10 --max-time 30 test.hurl
# HTML 测试报告
hurl --test --report-html report/ test.hurl
# JUnit 报告
hurl --test --report-junit results.xml test.hurl
# 并行运行多个文件
hurl --test --parallel tests/*.hurl
进阶用法¶
值捕获(Capture)¶
从响应中提取值,供后续请求使用:
# 登录并捕获 token
POST {{baseUrl}}/auth/login
Content-Type: application/json
{
"email": "admin@example.com",
"password": "admin123"
}
HTTP 200
[Captures]
auth_token: jsonpath "$.access_token"
user_id: jsonpath "$.user.id"
csrf_token: header "X-CSRF-Token"
# 使用捕获的值
GET {{baseUrl}}/api/users/{{user_id}}
Authorization: Bearer {{auth_token}}
X-CSRF-Token: {{csrf_token}}
HTTP 200
[Asserts]
jsonpath "$.id" == {{user_id}}
断言大全¶
HTTP 200
[Asserts]
# 状态码
status == 200
# Header 断言
header "Content-Type" == "application/json; charset=utf-8"
header "Cache-Control" contains "no-cache"
header "X-Request-Id" exists
header "X-Deprecated" not exists
# Body 断言
body contains "success"
body startsWith "{"
# JSON 断言
jsonpath "$.status" == "ok"
jsonpath "$.count" > 0
jsonpath "$.count" <= 100
jsonpath "$.items" count == 10
jsonpath "$.name" isString
jsonpath "$.active" isBoolean
jsonpath "$.data" isCollection
jsonpath "$.tags" includes "important"
jsonpath "$.email" matches "^[a-z]+@[a-z]+\\.[a-z]+$"
# 响应时间断言
duration < 2000
# 证书断言
certificate "Subject" contains "example.com"
# SHA-256 校验
sha256 == hex,e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855;
JSONPath 和 XPath¶
# JSON 响应断言
GET {{baseUrl}}/api/users
HTTP 200
[Asserts]
jsonpath "$" count == 10
jsonpath "$[0].name" exists
jsonpath "$[?(@.role=='admin')]" count >= 1
# XML 响应断言
GET {{baseUrl}}/api/data.xml
HTTP 200
[Asserts]
xpath "//user/@name" == "Alice"
xpath "count(//item)" == 5
变量和模板¶
# 从命令行传入变量
hurl --variable baseUrl=http://localhost:3000 \
--variable email=test@example.com \
test.hurl
# 从文件读取变量
hurl --variables-file vars.env test.hurl
# 在 Hurl 文件中使用
POST {{baseUrl}}/auth/login
Content-Type: application/json
{
"email": "{{email}}",
"password": "{{password}}"
}
重试和轮询¶
# 等待服务就绪(最多重试 10 次,每次间隔 1 秒)
GET {{baseUrl}}/health
[Options]
retry: 10
retry-interval: 1000
HTTP 200
# 轮询异步任务
POST {{baseUrl}}/api/tasks
Content-Type: application/json
{
"type": "export"
}
HTTP 202
[Captures]
task_id: jsonpath "$.task_id"
# 轮询任务状态
GET {{baseUrl}}/api/tasks/{{task_id}}
[Options]
retry: 20
retry-interval: 2000
HTTP 200
[Asserts]
jsonpath "$.status" == "completed"
文件上传¶
# 上传文件
POST {{baseUrl}}/api/upload
[MultipartFormData]
file: file,sample.csv; text/csv
description: 测试文件上传
HTTP 200
[Asserts]
jsonpath "$.filename" == "sample.csv"
CI/CD 集成¶
GitHub Actions
name: API Tests
on: [push, pull_request]
jobs:
api-tests:
runs-on: ubuntu-latest
services:
app:
image: my-app:latest
ports: ['3000:3000']
steps:
- uses: actions/checkout@v4
- name: Install Hurl
run: |
curl -LO https://github.com/Orange-OpenSource/hurl/releases/latest/download/hurl_amd64.deb
sudo dpkg -i hurl_amd64.deb
- name: Run API tests
run: |
hurl --test \
--variable baseUrl=http://localhost:3000 \
--report-junit results.xml \
--report-html report/ \
tests/*.hurl
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: hurl-report
path: report/
组织测试文件¶
tests/
├── auth/
│ ├── login.hurl
│ ├── register.hurl
│ └── refresh-token.hurl
├── users/
│ ├── crud.hurl # 一个文件包含创建→读取→更新→删除的完整流程
│ ├── list.hurl
│ └── permissions.hurl
├── health.hurl
├── vars/
│ ├── dev.env
│ ├── staging.env
│ └── ci.env
└── run-all.sh
#!/bin/bash
# run-all.sh
hurl --test \
--variable baseUrl=${BASE_URL:-http://localhost:3000} \
--variables-file vars/${ENV:-dev}.env \
--report-html report/ \
--parallel \
tests/**/*.hurl
常见问题¶
Q1: Hurl 和 curl 有什么关系?¶
Hurl 底层使用 libcurl,所以它继承了 curl 的所有 HTTP 能力(HTTP/2、TLS、代理等)。但 Hurl 在 curl 之上添加了:文件格式、断言、值捕获、链式请求。
Q2: Hurl 和 Bruno 该选哪个?¶
- Hurl:纯 CLI,纯文本,适合 CI/CD 和自动化测试
- Bruno:有 GUI,适合日常开发和探索性测试
- 可以两者并用:开发时用 Bruno 探索 API,CI 中用 Hurl 自动化验证
Q3: 如何处理认证?¶
# Basic Auth
GET {{baseUrl}}/api/data
[BasicAuth]
admin: password123
# Bearer Token
GET {{baseUrl}}/api/data
Authorization: Bearer {{token}}
# Cookie
GET {{baseUrl}}/api/data
Cookie: session={{session_id}}
Q4: 如何调试失败的测试?¶
# 使用 --very-verbose 查看完整请求和响应
hurl --very-verbose test.hurl
# 使用 --error-format long 获取详细的错误信息
hurl --test --error-format long test.hurl
# 只运行特定请求(从第 N 个开始)
hurl --from-entry 3 test.hurl
Q5: Hurl 支持 GraphQL 吗?¶
支持,GraphQL 本质上是 POST 请求:
POST {{baseUrl}}/graphql
Content-Type: application/json
{
"query": "{ users { id name email } }"
}
HTTP 200
[Asserts]
jsonpath "$.data.users" count > 0
参考资源¶
| 资源 | 链接 |
|---|---|
| 官方网站 | https://hurl.dev |
| GitHub 仓库 | https://github.com/Orange-OpenSource/hurl |
| 语法文档 | https://hurl.dev/docs/manual.html |
| 断言参考 | https://hurl.dev/docs/asserting-response.html |
| 示例集合 | https://hurl.dev/docs/samples.html |
| VS Code 扩展 | 在扩展市场搜索 "Hurl" |
总结:Hurl 是 API 测试领域的"简洁主义"代表。它的纯文本格式几乎没有学习成本,内置的断言和捕获能力覆盖了大多数测试场景。如果你需要在 CI/CD 中自动化 API 测试,或者想要一个比 curl 更结构化、比 Postman 更轻量的工具,Hurl 是最佳选择。