Files
junhong_cmp_fiber/.opencode/skills/hurl-test/SKILL.md
huang 2b408230a6
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m0s
skill
2026-03-31 10:00:01 +08:00

32 KiB
Raw Blame History

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: 验证  →  Phase 5: 研判
读代码搞清楚      展示给用户确认      输出 .hurl 文件     语法检查+试跑      分析失败/报告发现

核心原则:测试是用来发现问题的,不是用来全部通过的。 断言基于代码定义Handler + DTO生成与实际行为的差异是"发现",不是要修掉的"错误"。


Phase 1: 探索Explore

目标:读代码,搞清楚项目约定 + 涉及的接口 + 字段 + 依赖。要求准确,不求快。

真理源层级(必须遵守)

断言的字段名和类型基于以下优先级确定,高优先级推翻低优先级

优先级 来源 说明 怎么找
1最高 Handler 实际实现 Handler 返回什么类型、调用哪个 response 方法、是否做了 model→DTO 转换 读 handler 函数体,看 return response.Success(c, xxx) 中 xxx 的类型
2 DTO / Response 结构体 Handler 引用的 DTO 的 json tag、字段类型 读 DTO struct 定义
3最低 路由注册的 Output 声明 RouteSpec 中声明的 Output 类型,仅作为文档参考,可能过期 读路由注册代码

关键规则

  • 不能只读 DTO 就生成断言。必须追溯到 Handler 确认它确实用了这个 DTO
  • 如果 Handler 直接返回 GORM model 而非 DTO断言用 model 的字段名(可能是大写 ID 而非小写 id
  • 如果 Handler 用了 response.SuccessWithPagination(),分页字段以该函数的结构为准,而非 DTO 定义的分页结构
  • 当 Handler 实现与 DTO 定义不一致时,以 Handler 为准生成断言,同时在 Phase 5 中将不一致标记为发现

1.1 项目画像(首次使用时必须执行,后续复用)

首次为项目生成 Hurl 测试时,先回答以下问题(通过读代码,不要猜):

问题 怎么找
语言/框架 看 go.mod / package.json / requirements.txt / pom.xml
路由注册在哪 搜索 routerapp.Get@GetMapping@app.route 等关键词
请求/响应 schema 定义在哪 搜索 DTO / schema / serializer / model 目录,看 json tag 或装饰器
统一响应格式是什么 找 response helper 文件(如 response.goresponse.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
  • 是否 omitemptyjson:",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 验证已删除

唯一值防冲突规范(强制执行)

所有创建类请求POST凡是有唯一约束的字段必须使用 {{newUuid}} 或包含 UUID 后缀,确保测试可重复运行。

场景 正确做法 错误做法
编码/Code 字段 "code": "TEST-{{newUuid}}" "code": "TEST-001"
名称(有唯一约束) "name": "测试-{{newUuid}}" "name": "测试角色"
用户名 "username": "hurl_{{newUuid}}" "username": "test_user"
手机号 "phone": "189{{newUuid}}" 取前11位或放在 env 文件 "phone": "18899990001"
邮箱 "email": "{{newUuid}}@test.com" "email": "test@test.com"

后续步骤需要引用的值(如登录用的 username/password

  • 必须定义在 env/dev.env 中作为变量
  • 在创建请求和后续引用处都通过 {{变量名}} 引用,保持一致
  • env 文件中的值也应该带有测试标识前缀(如 hurl_test_),避免与真实数据冲突
  • env 中有唯一约束的变量值每次运行前可能需要更新(文档中注明)

业务流程模式

  • 按用户描述的流程顺序编排请求
  • 上一步的输出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/                 # 接口契约验证
├── reports/                  # Phase 5 发现报告(自动生成)
│   └── findings-{name}-{date}.md
└── 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 5: 研判Triage

目标:分析测试失败原因,区分"测试自身问题"和"接口问题",生成发现报告。

核心原则:不因实际返回与预期不符而盲目修改断言。差异是"发现",需要分类研判。

5.1 失败分类A/B/C 三类)

每个断言失败必须归入以下三类之一:

类型 含义 判断标准 处置方式
A 类 — 测试自身错误 JSON path 写错、请求参数遗漏、env 变量缺失、前置步骤失败导致 ID 为空 改正后 DTO/Handler 代码能对应上 自动修正测试,不打扰用户
B 类 — 代码一致性问题 DTO 定义和 Handler 实际返回不一致(如 DTO 写 json:"id" 但 Handler 返回 "ID"DTO 定义了分页结构但 Handler 用了另一种) DTO 说的是 AHandler 做的是 B两个都是代码 ⚠️ 以 Handler 为准修正断言 + 写入发现报告
C 类 — 接口行为 Bug 接口行为不符合合理预期(如详情接口缺少列表接口有的字段;创建接口返回 500同一个 DTO 在不同接口表现不一致) Handler 的行为本身不合理,不是 DTO 过期的问题 🐛 保留原始断言(测试会失败) + 写入发现报告

5.2 分类判断流程

断言失败
  │
  ├─ jsonpath 写错 / 变量未定义 / 前置步骤失败?
  │   → A 类:修正测试
  │
  ├─ DTO 定义字段名 ≠ 实际返回字段名?(如 id vs ID, list vs items
  │   │
  │   ├─ Handler 代码确实返回了不同的结构?
  │   │   → B 类:以 Handler 为准,报告不一致
  │   │
  │   └─ Handler 代码引用了 DTO 但行为不符?
  │       → C 类:接口 Bug
  │
  ├─ 某个字段在接口 A 有、接口 B 没有?(如列表有但详情没有)
  │   → C 类:接口一致性 Bug
  │
  ├─ 接口返回 5xx 错误?
  │   → C 类:服务端 Bug
  │
  └─ 业务逻辑不符预期?(如已传 packages 但响应中 packages 为空)
      → C 类:业务逻辑 Bug

5.3 发现报告格式

报告输出到 tests/hurl/reports/findings-{测试文件名}-{日期}.md,格式如下:

# 🔍 Hurl 测试发现报告

- 测试文件:`flows/package-resource-full-flow.hurl`
- 生成时间2026-03-30
- 总请求数25
- 通过20
- 失败5A类: 2, B类: 1, C类: 2

---

## A 类(测试自身错误)— 已自动修正

### A-1: 角色名冲突导致创建失败
- **位置**:第 2 步 POST /api/admin/roles
- **原因**:角色名使用固定值,重复运行时 409 冲突
- **修正**:角色名改为 `hurl测试角色-{{newUuid}}`

---

## B 类(代码一致性问题)— 已按 Handler 修正断言

### B-1: 角色接口返回原始 GORM 模型而非 DTO
- **位置**POST /api/admin/roles → 响应
- **DTO 定义**`RoleResponse``json:"id"`(小写)
- **Handler 实际**:直接返回 `model.Role`,字段为 `ID`(大写 GORM 默认)
- **影响**:前端按 API 文档对接会取不到 `id` 字段
- **建议**Handler 添加 model → DTO 转换

---

## C 类(接口行为 Bug— 断言保留,测试会失败

### C-1: 套餐详情接口缺少 one_time_commission_amount
- **位置**GET /api/admin/packages/:id代理视角
- **预期**`PackageResponse` DTO 定义了 `one_time_commission_amount` 字段
- **实际**:列表接口 GET /packages 返回该字段,详情接口不返回
- **影响**:代理端查看单个套餐时看不到佣金信息
- **建议**Detail handler 增加佣金信息增强逻辑

### C-2: 批量分配对已授权店铺返回 500
- **位置**POST /api/admin/shop-package-batch-allocations
- **预期**对已有授权的店铺应返回业务错误码4xx而非 500
- **实际**:返回 `{code: 2001, msg: "内部服务器错误"}`
- **影响**:前端无法区分是参数错误还是服务异常
- **建议**:添加"已存在授权"的前置检查,返回明确的业务错误

5.4 研判完成后的交付物

交付物 说明
修正后的 .hurl 文件 A 类已自动修正B 类按 Handler 实际修正C 类保留原始断言(预期失败)
发现报告 tests/hurl/reports/findings-{name}-{date}.md
终端摘要 向用户输出发现数量和关键 C 类问题摘要

5.5 用户交互

研判完成后,向用户展示摘要并询问:

## 研判结果

- A 类测试错误N 个,已自动修正
- B 类一致性问题N 个,已按实际修正并记录
- C 类(接口 BugN 个,断言保留(测试会失败直到代码修复)

### C 类发现摘要:
1. [C-1] 套餐详情缺少 one_time_commission_amount
2. [C-2] 批量分配对已授权店铺返回 500

详细报告已写入: tests/hurl/reports/findings-xxx.md

是否需要对某个 C 类发现调整处置?(如确认为已知行为,改为 B 类)

红线规则

规则 说明
不跳过 Phase 1 必须读代码确认接口路径和字段,不能凭记忆或猜测
不跳过 Phase 2 必须向用户展示发现的接口和字段,确认后再生成
不编造字段 所有断言的字段名必须来自实际 schema 代码
不编造路径 所有接口路径必须来自实际路由代码
不遗漏字段 schema 中每个参与序列化的字段都必须有对应断言
不硬编码 ID 所有依赖的 ID 通过 [Captures] 从上游请求获取
不假设响应格式 统一响应结构必须从代码中确认,不同项目格式不同
唯一值防冲突 创建类请求的所有唯一约束字段必须使用 {{newUuid}}(详见 3.2 规范)
自给自足 每个 .hurl 文件自己创建测试数据,不依赖外部数据准备
不为通过而改断言 断言失败时走 Phase 5 研判流程C 类 Bug 必须保留断言并写入发现报告
Handler 追溯 断言字段名必须追溯到 Handler 实际返回的类型,不能只看 DTO 定义(详见 Phase 1 真理源层级)

AI 助手检查清单

生成阶段自检Phase 3 完成后)

  1. 文件头注释包含流程描述和前置条件
  2. 认证步骤正确 capture 了 token / cookie
  3. 所有依赖数据通过 API 链式创建(自给自足)
  4. 每个成功响应断言了项目的统一响应格式
  5. 查询详情接口逐字段断言(类型 + 值,基于 Handler 实际返回)
  6. 分页接口断言了分页结构 + 第一条记录的字段
  7. 修改操作后紧跟 GET 验证修改生效
  8. 删除操作后紧跟 GET 验证已删除
  9. 所有创建请求的唯一约束字段使用了 {{newUuid}}
  10. {{变量}} 引用无悬空(都有 env 或 capture 来源)
  11. 文件放在了正确的目录位置
  12. 特殊场景有明确的处理策略和注释提醒
  13. 断言字段名已追溯到 Handler 实际返回类型(非仅依赖 DTO

研判阶段自检Phase 5 完成后)

  1. 每个断言失败都已分类A/B/C
  2. A 类错误已自动修正
  3. B 类不一致已按 Handler 修正断言,并写入发现报告
  4. C 类 Bug 保留了原始断言(测试预期失败),并写入发现报告
  5. 发现报告已输出到 tests/hurl/reports/findings-{name}-{date}.md
  6. 向用户展示了研判结果摘要

附录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
BODYJSON / 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_pathJsonPath 关键字大小写敏感 必须小写 jsonpath
[Captures] 写成 [Capture] 必须是复数 [Captures][Asserts][Options]
变量 {{ var }} 有空格 允许,但建议统一 {{var}}{{ var }} 都可以
isIsoDate 用在非 RFC 3339 格式 只认 YYYY-MM-DDTHH:mm:ss 格式 如果是其他格式用 matches
file,path; 路径含 .. Hurl 禁止相对父目录 --file-root 或调整路径