跳转至

Hurl: 纯文本 HTTP 请求测试工具

为什么要学 Hurl

API 测试有两个极端:一端是 Postman 这样的 GUI 工具(重、难以自动化),另一端是 curl 命令(轻、但难以组织和断言)。Hurl 在中间找到了平衡点:用纯文本文件描述 HTTP 请求和断言,像写测试用例一样测试 API

特性curlPostmanHurl
格式命令行参数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

brew install hurl

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

# Chocolatey
choco install hurl

# Scoop
scoop install hurl

# WinGet
winget install Orange.Hurl

验证安装

hurl --version
# hurl 6.x.x

# 快速测试
echo 'GET https://httpbin.org/get' | hurl

编辑器支持

  • VS Code:安装 "Hurl" 扩展(语法高亮 + 运行)
  • JetBrains:安装 "Hurl" 插件
  • Vim/Neovim:tree-sitter 支持 hurl 语法

快速上手

第一个 Hurl 文件

# hello.hurl

# 简单的 GET 请求
GET https://httpbin.org/get

# 断言状态码
HTTP 200

运行:

hurl hello.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
# vars.env
baseUrl=http://localhost:3000
email=test@example.com
password=secret123
# 在 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 是最佳选择。