23 KiB
name, description
| name | description |
|---|---|
| hurl-test | Hurl 接口测试生成器。用户描述要测试的接口或业务流程,自动探索代码、确认需求、生成完整的 .hurl 测试文件(含 DTO 驱动的字段完整性断言)。触发词:测试、hurl、写测试、接口测试。 |
Hurl 接口测试生成器
用户描述要测试什么,你来读代码、问确认、生成 .hurl 文件。
适用于任何后端项目(Go / Python / Node / Java 等),不预设框架和目录结构。
触发条件
以下情况必须使用本 Skill:
- 用户说"测试 XX 接口"、"写 hurl 测试"、"给 XX 加测试"
- 用户说"测试 XX 流程"、"测试 XX 的业务逻辑"
- 用户说"验证 XX 接口的字段"、"测试接口契约"
- 用户提到 hurl、.hurl、接口测试、集成测试、冒烟测试
四阶段工作流(必须按顺序执行)
Phase 1: 探索 → Phase 2: 确认 → Phase 3: 生成 → Phase 4: 验证
读代码搞清楚 展示给用户确认 输出 .hurl 文件 语法检查 + 试跑
Phase 1: 探索(Explore)
目标:读代码,搞清楚项目约定 + 涉及的接口 + 字段 + 依赖。
1.1 项目画像(首次使用时必须执行,后续复用)
首次为项目生成 Hurl 测试时,先回答以下问题(通过读代码,不要猜):
| 问题 | 怎么找 |
|---|---|
| 语言/框架 | 看 go.mod / package.json / requirements.txt / pom.xml |
| 路由注册在哪 | 搜索 router、app.Get、@GetMapping、@app.route 等关键词 |
| 请求/响应 schema 定义在哪 | 搜索 DTO / schema / serializer / model 目录,看 json tag 或装饰器 |
| 统一响应格式是什么 | 找 response helper 文件(如 response.go、response.py),记录 JSON 结构 |
| 认证方式是什么 | 找 auth middleware,确定是 Bearer Token / Cookie / API Key / Basic Auth |
| 登录接口是什么 | 找登录 handler,记录路径、请求体、响应中 token 的位置 |
| 分页格式是什么 | 找列表接口的响应结构,记录 items/total/page 等字段名 |
| 已有 hurl 测试吗 | 搜索 *.hurl 文件,复用已有的约定 |
将画像结果写入 tests/hurl/.project-profile.md 文件持久化保存。
画像持久化(关键机制)
首次使用时:完成 1.1 探索后,将画像写入 tests/hurl/.project-profile.md,格式如下:
# 项目画像(Hurl 测试自动生成用)
<!-- 由 hurl-test skill 自动生成,请勿手动修改 -->
<!-- 如需刷新,删除此文件后重新运行 skill -->
## 技术栈
- 语言: Go 1.25
- 框架: Fiber v2
- ORM: GORM
## 路由定义位置
- 路由注册入口: internal/routes/routes.go
- 按模块拆分: internal/routes/{module}.go
- 路由注册函数: Register(router, doc, basePath, method, path, handler, spec)
## Schema 定义位置
- DTO 目录: internal/model/dto/
- 命名规则: {module}_dto.go
- 字段标签: json / validate / description
## 统一响应格式
{code: int, msg: string, data: any, timestamp: string(RFC3339)}
- 成功: code=0, msg="success"
- 错误: code!=0
## 分页格式
{items: [], total: int, page: int, size: int}
- 包裹在 data 字段内: $.data.items / $.data.total
## 认证方式
- 后台: POST /api/auth/admin-login → $.data.access_token → Authorization: Bearer {token}
- C端: JWT → Authorization: Bearer {token}
## 默认测试账号
- 用户名: admin
- 密码: Admin@123456
## 服务端口
- 默认: 3000
后续使用时:检查 tests/hurl/.project-profile.md 是否存在:
- 存在 → 直接读取,跳过 1.1 的探索步骤,节省时间
- 不存在 → 执行 1.1 完整探索,然后生成此文件
- 用户说"刷新画像" → 删除旧文件,重新执行 1.1
1.2 找接口定义
根据用户要测的模块,定位路由注册代码,提取:
- HTTP 方法(GET / POST / PUT / DELETE / PATCH)
- 路由路径(含路径参数格式,如
/users/:id或/users/{id}) - 接口说明(注释、Summary、装饰器描述)
- 是否需要认证
- 请求 schema 类型名(Input / Request DTO)
- 响应 schema 类型名(Output / Response DTO)
1.3 读 schema 定义(DTO / struct / class / type)
定位请求和响应的 schema 定义文件,提取每个字段的:
- 字段名:JSON 序列化后的名称(json tag / @JsonProperty / serializer field)
- 语言类型:string / int / bool / 数组 / 嵌套对象 / 可空等
- 是否必填:validate tag / required 装饰器 / 非空标注
- 是否可空:指针类型 / Optional / nullable
- 是否参与序列化:
json:"-"/ @JsonIgnore / exclude - 是否 omitempty:
json:",omitempty"/ 条件序列化 - 字段描述:description tag / docstring / 注释
1.4 识别业务依赖
读 service / business logic 层,识别:
- 创建操作需要哪些前置数据(如创建订单需要先有商品和用户)
- 是否有唯一性约束(如用户名不能重复)
- 是否依赖外部服务(支付网关、短信、OAuth 等)
- 业务流转逻辑(状态机、级联操作)
Phase 2: 确认(Clarify)
目标:向用户展示发现的内容,确认模糊点。不要闷头生成。
2.1 必须展示的内容
我梳理了相关代码,发现以下信息:
📋 涉及接口:
- [方法] [路径] - [说明](认证: 是/否)
- ...
📦 响应字段(基于 {SchemaName}):
- [字段名]: [类型] - [说明]
- ...(共 N 个字段,将全部生成断言)
🔗 依赖关系:
- [创建 X 需要先创建 Y]
- ...
⚠️ 特殊情况:
- [涉及外部服务 / 文件上传 / 特殊认证等]
2.2 按需确认(只问有歧义的)
| 场景 | 要问的 |
|---|---|
| 流程范围不明确 | "要测到哪一步?" |
| 多种用户角色 | "用哪种身份测?" |
| 是否测异常 | "需要包含异常 case 吗?(参数校验失败、权限不足等)" |
| 是否测数据隔离 | "需要验证不同用户间数据不可见吗?" |
| 涉及第三方 | "XX 部分怎么处理?绕过 / 模拟回调 / 跳过?" |
| 前置数据来源 | "XX 依赖数据是通过 API 创建还是假设已存在?" |
如果用户说"越完整越好"或"都要"→ 默认全部包含,不再追问。
Phase 3: 生成(Generate)
目标:生成完整的 .hurl 文件,字段断言基于 schema 代码,不能编造。
3.1 文件头注释
# ============================================================
# 测试:{测试名称}
# 生成时间:{日期}
# 涉及模块:{module1, module2, ...}
# 涉及接口:{N} 个
# 断言数量:{N} 条
# 前置条件:{服务运行 + 必要的前置条件}
# ============================================================
# 流程:
# 1. {步骤描述}
# 2. {步骤描述}
# ...
# ============================================================
3.2 请求生成规则
认证:
- 根据 Phase 1 画像中的登录接口和 token 位置生成
- token 必须通过
[Captures]捕获,后续请求引用 - 如果是 Cookie 认证,用
[Cookies]或 cookie capture
CRUD 标准模式:
| 操作 | 生成要求 |
|---|---|
| 创建(POST) | capture 返回的 ID;唯一字段用 {{newUuid}} 防冲突 |
| 查询详情(GET) | 逐字段断言(类型 + 值,见 3.3) |
| 查询列表(GET) | 分页结构断言 + items[0] 逐字段断言 |
| 修改(PUT/PATCH) | 修改后紧跟一个 GET 验证修改生效 |
| 删除(DELETE) | 删除后紧跟一个 GET 验证已删除 |
业务流程模式:
- 按用户描述的流程顺序编排请求
- 上一步的输出(ID、状态等)通过
[Captures]传给下一步 - 关键步骤加中间状态验证(如创建订单后验证状态为"待支付")
3.3 schema 到断言的映射
读到 schema 字段后,按以下规则生成 jsonpath 断言:
通用类型映射(所有语言):
| Schema 类型特征 | Hurl 断言 |
|---|---|
| 字符串(string / str / String) | isString |
| 整数(int / integer / long / Int) | isInteger |
| 浮点(float / double / decimal / Float) | isNumber |
| 布尔(bool / boolean / Boolean) | isBoolean |
| 数组 / 列表([] / List / Array) | isList |
| 嵌套对象(struct / class / dict / object) | isObject,并递归检查子字段 |
| 可空类型(指针 / Optional / nullable) | exists(不强制类型,因为可能是 null) |
| 不参与序列化(json:"-" / @JsonIgnore / exclude=True) | 跳过,不生成断言 |
| 条件序列化(omitempty / if not None) | exists 或不生成(取决于场景) |
Go 特定映射:
| Go 类型 | Hurl 断言 |
|---|---|
string |
isString |
int, int8/16/32/64, uint, uint8/16/32/64 |
isInteger |
float32, float64 |
isNumber |
bool |
isBoolean |
[]T |
isList |
*string, *int, *uint 等指针 |
exists |
time.Time |
isString(通常序列化为字符串) |
map[string]any |
isObject |
Python 特定映射(Pydantic / Django / FastAPI):
| Python 类型 | Hurl 断言 |
|---|---|
str |
isString |
int |
isInteger |
float, Decimal |
isNumber |
bool |
isBoolean |
list[T], List[T] |
isList |
Optional[T], `T |
None` |
dict, Dict |
isObject |
datetime, date |
isString |
TypeScript/JavaScript 特定映射:
| TS/JS 类型 | Hurl 断言 |
|---|---|
string |
isString |
number(整数上下文) |
isInteger |
number(通用) |
isNumber |
boolean |
isBoolean |
T[], Array<T> |
isList |
T | null, T | undefined |
exists |
object, Record<> |
isObject |
Date |
isString |
Java 特定映射:
| Java 类型 | Hurl 断言 |
|---|---|
String |
isString |
Integer, Long, int, long |
isInteger |
Double, Float, BigDecimal |
isNumber |
Boolean, boolean |
isBoolean |
List<T> |
isList |
@Nullable, Optional<T> |
exists |
Map<K,V> |
isObject |
LocalDateTime, Instant |
isString |
3.4 统一响应格式断言
根据 Phase 1 画像中发现的统一响应格式,为每个成功响应添加格式断言。
示例:如果项目的统一格式是 {code, msg, data, timestamp}:
[Asserts]
jsonpath "$.code" == 0
jsonpath "$.msg" == "success"
jsonpath "$.timestamp" isIsoDate
示例:如果项目的格式是 {status, message, result}:
[Asserts]
jsonpath "$.status" == "ok"
jsonpath "$.message" isString
示例:如果项目无统一包装,直接返回数据:
[Asserts]
# 直接断言业务字段
jsonpath "$.id" isInteger
jsonpath "$.name" isString
不要假设响应格式,必须从代码中确认。
3.5 分页断言
根据 Phase 1 画像中发现的分页结构生成。
示例:如果是 {items, total, page, size} 格式:
jsonpath "$.data.items" isList
jsonpath "$.data.total" isInteger
jsonpath "$.data.total" >= 1
jsonpath "$.data.page" isInteger
jsonpath "$.data.size" isInteger
# items 内元素逐字段断言
jsonpath "$.data.items[0].{field}" {type_assert}
示例:如果是 {results, count, next, previous} 格式(Django 风格):
jsonpath "$.results" isList
jsonpath "$.count" isInteger
jsonpath "$.count" >= 1
# results 内元素逐字段断言
jsonpath "$.results[0].{field}" {type_assert}
根据实际代码调整字段名,不硬编码。
3.6 异常 Case 模板
参数校验失败:
# ── 异常:参数校验失败 ──
POST {{base_url}}/{path}
Authorization: Bearer {{token}}
Content-Type: application/json
{
"required_field": ""
}
HTTP {expected_error_status}
[Asserts]
# 断言错误响应格式(根据项目约定调整)
HTTP 状态码根据项目实际返回确定:有的项目错误也返回 200 + 业务错误码,有的返回 400/422。
未认证访问:
# ── 异常:未认证访问 ──
GET {{base_url}}/{protected_path}
HTTP {expected_unauth_status}
越权访问(如果用户要求):
# ── 异常:用户 B 不能访问用户 A 的资源 ──
GET {{base_url}}/{path}/{{user_a_resource_id}}
Authorization: Bearer {{user_b_token}}
HTTP {expected_forbidden_status}
3.7 特殊场景处理
| 场景 | 处理策略 |
|---|---|
| 短信/邮件验证码 | 建议服务端加 test_mode 开关,固定验证码写入 env 文件;注释提醒用户 |
| 第三方支付 | 优先用项目内部支付方式(如钱包支付);如需测回调,直接 POST 回调接口模拟 |
| OAuth 登录(微信/Google/GitHub) | 建议服务端加 test_mode 支持直接传 openid/email;注释提醒用户 |
| 文件上传 | 用 Hurl 的 [Multipart] 语法 + testdata 目录下的样本文件 |
| 外部 API 依赖 | 只测参数校验和错误响应格式,不断言业务结果;注释说明依赖 |
| WebSocket | Hurl 不支持,注释说明跳过 |
| 异步任务结果 | 用 Hurl 的 retry + retry-interval 轮询直到状态变更 |
异步轮询示例:
# 等待异步任务完成(最多重试 10 次,间隔 500ms)
GET {{base_url}}/{path}/{{task_id}}
Authorization: Bearer {{token}}
[Options]
retry: 10
retry-interval: 500ms
HTTP 200
[Asserts]
jsonpath "$.data.status" == "completed"
3.8 文件输出
目录结构(首次使用时创建,如不存在):
tests/hurl/
├── env/
│ └── dev.env # 环境变量
├── testdata/ # 测试用的样本文件
├── flows/ # 业务流程测试
├── modules/ # 按模块的接口测试
│ └── {module}/
│ └── 01-crud.hurl
├── negative/ # 异常/边界测试
├── contract/ # 接口契约验证
└── Makefile # 快捷命令
文件放置规则:
| 用户描述 | 输出路径 |
|---|---|
| 测试 XX 流程 / 业务流程 | tests/hurl/flows/{flow-name}.hurl |
| 测试 XX 模块的 CRUD / 接口 | tests/hurl/modules/{module}/01-crud.hurl |
| 测试异常/边界/权限 | tests/hurl/negative/{name}.hurl |
| 测试接口契约/字段对齐 | tests/hurl/contract/{name}.hurl |
如果项目已有 hurl 测试目录结构,沿用已有约定,不要另起炉灶。
3.9 env 和 Makefile
env/dev.env(首次创建时生成,内容基于 Phase 1 画像):
# 服务地址
base_url=http://localhost:{port}
# 认证信息(根据项目实际填写)
admin_username={默认用户名}
admin_password={默认密码}
# 测试模式变量(如果有特殊场景)
# test_sms_code=888888
# test_openid=test_openid_001
Makefile(首次创建时生成):
SHELL := /bin/bash
ENV ?= dev
HURL_OPTS := --variables-file env/$(ENV).env --test
.PHONY: test test-flows test-modules test-negative report
test: ## 运行所有测试
hurl $(HURL_OPTS) .
test-flows: ## 运行业务流程测试
hurl $(HURL_OPTS) flows/
test-modules: ## 运行模块接口测试
hurl $(HURL_OPTS) modules/
test-negative: ## 运行异常测试
hurl $(HURL_OPTS) negative/
report: ## 生成 HTML 报告
hurl $(HURL_OPTS) --report-html build/report/ .
Phase 4: 验证(Verify)
目标:确保生成的 .hurl 文件语法正确、可运行。
4.1 语法自检
- 每个请求之间有空行分隔
[Captures]和[Asserts]拼写正确(大小写敏感)- 所有
{{变量}}引用都有来源(env 文件定义 或 上游[Captures]) - JSON body 无尾逗号、格式正确
HTTP {status}在请求之后、[Captures]/[Asserts]之前- 请求和断言之间没有多余空行(
HTTP行必须紧跟请求) - 文件上传路径相对于 hurl 文件位置正确
4.2 运行测试
hurl --variables-file tests/hurl/env/dev.env --test tests/hurl/{生成的文件}
- 服务在跑 → 执行,如有失败分析修正
- 服务没跑 → 跳过,告知用户手动验证命令
红线规则
| 规则 | 说明 |
|---|---|
| 不跳过 Phase 1 | 必须读代码确认接口路径和字段,不能凭记忆或猜测 |
| 不跳过 Phase 2 | 必须向用户展示发现的接口和字段,确认后再生成 |
| 不编造字段 | 所有断言的字段名必须来自实际 schema 代码 |
| 不编造路径 | 所有接口路径必须来自实际路由代码 |
| 不遗漏字段 | schema 中每个参与序列化的字段都必须有对应断言 |
| 不硬编码 ID | 所有依赖的 ID 通过 [Captures] 从上游请求获取 |
| 不假设响应格式 | 统一响应结构必须从代码中确认,不同项目格式不同 |
| 唯一值防冲突 | 创建类请求的唯一字段使用 {{newUuid}} 或 {{newDate}} |
| 自给自足 | 每个 .hurl 文件自己创建测试数据,不依赖外部数据准备 |
AI 助手检查清单
生成 .hurl 文件后自检:
- ✅ 文件头注释包含流程描述和前置条件
- ✅ 认证步骤正确 capture 了 token / cookie
- ✅ 所有依赖数据通过 API 链式创建(自给自足)
- ✅ 每个成功响应断言了项目的统一响应格式
- ✅ 查询详情接口逐字段断言(类型 + 值,基于 schema)
- ✅ 分页接口断言了分页结构 + 第一条记录的字段
- ✅ 修改操作后紧跟 GET 验证修改生效
- ✅ 删除操作后紧跟 GET 验证已删除
- ✅ 唯一字段使用了
{{newUuid}} - ✅
{{变量}}引用无悬空(都有 env 或 capture 来源) - ✅ 文件放在了正确的目录位置
- ✅ 特殊场景有明确的处理策略和注释提醒
附录:Hurl 语法速查
生成 .hurl 文件时必须参照本速查,不可凭记忆编造语法。
文件结构
一个 .hurl 文件由多个 entry 组成,每个 entry = 请求 + 可选响应:
请求1
响应1(可选)
请求2
响应2(可选)
entry 之间用空行分隔。
请求格式
METHOD URL
Header1: value1
Header2: value2
[Options]
key: value
[Query]
param1: value1
[Form]
field1: value1
[Multipart]
file1: file,path/to/file;
[BasicAuth]
username: password
[Cookies]
name: value
BODY(JSON / XML / multiline string / file)
规则:
- Method + URL 是第一行,必须
- Headers 紧跟 URL 之后(无 section 标记)
- Sections(
[Query]、[Form]、[Options]等)顺序任意 - Body 必须在最后
- JSON body 直接写
{ }即可,自动设置 Content-Type: application/json
响应格式
HTTP {status_code}
Header1: expected_value1
[Captures]
var_name: jsonpath "$.path"
[Asserts]
jsonpath "$.field" == "value"
规则:
HTTP {status}紧跟请求之后(中间不能有空行)HTTP *表示不检查状态码- Headers 检查紧跟 HTTP 行之后
[Captures]和[Asserts]顺序任意
变量和模板
# 引用变量(从 env 文件、命令行或上游 capture 获取)
GET {{base_url}}/api/users/{{user_id}}
# 内置函数
POST {{base_url}}/api/users
{
"email": "{{newUuid}}@test.com",
"created_at": "{{newDate}}"
}
可用函数:
{{newUuid}}— 生成 UUID v4{{newDate}}— 生成 RFC 3339 UTC 时间戳
Capture 语法
[Captures]
# JSONPath
token: jsonpath "$.data.access_token"
user_id: jsonpath "$.data.id"
first_item: jsonpath "$.items[0].name"
# Header
location: header "Location"
# Cookie
session: cookie "SESSIONID"
# Body(整个响应体作为字符串)
full_body: body
# Status code
code: status
# 正则表达式
csrf: regex "name=\"csrf\" value=\"([^\"]+)\""
# 响应时间(毫秒)
response_time: duration
Assert 语法
[Asserts]
# ── 状态码 ──
status == 200
status >= 200
status < 300
# ── JSONPath 断言 ──
jsonpath "$.name" == "Alice" # 等于
jsonpath "$.name" != "Bob" # 不等于
jsonpath "$.age" > 18 # 大于
jsonpath "$.age" >= 18 # 大于等于
jsonpath "$.count" < 100 # 小于
jsonpath "$.items" count == 5 # 集合长度
jsonpath "$.name" startsWith "Al" # 前缀
jsonpath "$.name" endsWith "ce" # 后缀
jsonpath "$.name" contains "lic" # 包含
jsonpath "$.date" matches /\\d{4}-\\d{2}-\\d{2}/ # 正则
# ── 类型断言 ──
jsonpath "$.name" isString
jsonpath "$.age" isInteger
jsonpath "$.score" isFloat
jsonpath "$.count" isNumber # 整数或浮点
jsonpath "$.active" isBoolean
jsonpath "$.items" isList
jsonpath "$.meta" isObject
jsonpath "$.id" isUuid
jsonpath "$.created_at" isIsoDate # RFC 3339 格式
jsonpath "$.field" isEmpty # 空集合
# ── 存在性 ──
jsonpath "$.field" exists
jsonpath "$.field" not exists
# ── 否定 ──
jsonpath "$.name" not contains "Bob"
jsonpath "$.status" not == "deleted"
# ── Header 断言 ──
header "Content-Type" contains "application/json"
header "X-Request-Id" exists
# ── 性能 ──
duration < 1000 # 响应时间(毫秒)
# ── Body 断言 ──
body contains "Hello"
bytes count == 1024
Options(逐请求配置)
GET {{base_url}}/api/task/{{task_id}}
[Options]
retry: 10 # 最大重试次数(-1 = 无限)
retry-interval: 500ms # 重试间隔
delay: 2s # 请求前等待
location: true # 跟随重定向
insecure: true # 允许不安全 SSL
verbose: true # 输出详细日志
very-verbose: true # 输出更详细日志
skip: true # 跳过此请求
variable: key=value # 定义变量
HTTP 200
Multipart 文件上传
POST {{base_url}}/api/upload
[Multipart]
file: file,testdata/sample.xlsx;
field1: value1
# 指定 Content-Type
file2: file,testdata/data.bin; application/octet-stream
运行命令
# 运行单个文件
hurl --test file.hurl
# 带变量文件
hurl --variables-file env/dev.env --test file.hurl
# 运行目录下所有 .hurl
hurl --test tests/hurl/
# 生成 HTML 报告
hurl --test --report-html build/report/ tests/hurl/
# 生成 JUnit 报告(CI 用)
hurl --test --report-junit build/report.xml tests/hurl/
# 并行执行(--test 默认并行,同文件内串行)
hurl --test --jobs 4 tests/hurl/
# 指定单个变量
hurl --variable base_url=http://localhost:3000 --test file.hurl
# 失败后继续执行
hurl --test --continue-on-error tests/hurl/
常见错误
| 错误 | 原因 | 修正 |
|---|---|---|
HTTP 200 和请求之间有空行 |
空行会被当作 entry 分隔符 | 删除空行,HTTP 行紧跟请求 |
| JSON body 有尾逗号 | Hurl 严格解析 JSON | 删除最后一个逗号 |
jsonpath 写成 json_path 或 JsonPath |
关键字大小写敏感 | 必须小写 jsonpath |
[Captures] 写成 [Capture] |
必须是复数 | [Captures]、[Asserts]、[Options] |
变量 {{ var }} 有空格 |
允许,但建议统一 | {{var}} 或 {{ var }} 都可以 |
isIsoDate 用在非 RFC 3339 格式 |
只认 YYYY-MM-DDTHH:mm:ss 格式 |
如果是其他格式用 matches |
file,path; 路径含 .. |
Hurl 禁止相对父目录 | 用 --file-root 或调整路径 |