auto-test
API 自动化测试(Hurl + Apifox MCP)
概述
通过 Apifox MCP 读取接口文档 → AI 生成 Hurl 测试文件 → 执行真实 HTTP 请求 → 生成测试报告 → 失败项自动调用 /fix-bug 走标准修复流程。
前置依赖:
- Hurl CLI(
brew install hurl/winget install hurl/apt install hurl) - Apifox MCP Server(已配置项目 ID 和 Access Token)
- 后端服务运行中
测试粒度
| 粒度 | 说明 | 文件命名 | 示例 |
|---|---|---|---|
| 单接口 | 一个 API 端点的正常+异常场景 | {接口名}.hurl |
create-order.hurl |
| 单业务组合 | 一个模块的 CRUD 生命周期 | {模块}-lifecycle.hurl |
order-lifecycle.hurl |
| 跨业务串联 | 多模块联动的完整业务流程 | {流程名}-flow.hurl |
menu-order-payment-flow.hurl |
文件结构
tests/hurl/
├── env/
│ ├── dev.env # 开发环境变量
│ ├── test.env # 测试环境
│ └── prod.env # 生产环境(只读接口)
├── {模块名}/ # 按业务模块组织
│ ├── {接口名}.hurl # 单接口测试
│ └── {模块}-lifecycle.hurl # 单业务组合
├── flows/ # 跨业务串联
│ └── {流程名}-flow.hurl
└── reports/ # 测试报告(gitignore)
├── index.html
└── report.json
系统接口请求响应规范
详细参考:
references/api-conventions.md(首次生成测试时必须读取)
核心契约速查
| 项目 | 规范 |
|---|---|
| 请求封装 | {"content": { ... }} (LeRequest) |
| 成功码 | code == 10000 |
| 认证头 | X-Token + merchant-id + Merchant-Id |
| 未授权 | 不带 X-Token → HTTP 401 |
返回类型 → data 结构映射
| Controller 返回类型 | data 结构 | 断言关键路径 |
|---|---|---|
Long(save) |
雪花 ID | $.data isInteger |
void(delete/submit/export) |
null | 仅验证 $.code == 10000 |
XxxVO(detail) |
对象 | $.data.id exists |
PageVO<T>(设置类 page) |
$.data.records[] |
$.data.records isCollection |
ReportBaseTotalVO<T>(报表 page) |
$.data.resultPage.records[] + $.data.totalLine |
分页 + 合计行 |
content 传值类型(最易出错)
| 场景 | content 类型 | 示例 |
|---|---|---|
| 分页查询 | 对象 | {"page":{"current":1,"size":10},"keyword":"xxx"} |
| 新增/编辑 | 对象 | {"name":"xxx","entries":[...]} |
| 详情/删除/提交 | 裸 Long | {{id}}(不是 {"id":{{id}}}) |
关键区别:detail/delete/submit 的 content 是直接传 ID 值,不需要包装成对象。
Hurl 语法速查
请求头规范(必须遵守)
每个需要认证的请求必须携带以下 Header:
| Header | 来源 | 用途 |
|---|---|---|
X-Token |
环境变量 {{x_token}}(配置文件提供) |
认证令牌 |
merchant-id |
环境变量 {{merchant_id}} |
商户路由(数据源切换) |
Merchant-Id |
环境变量 {{merchant_id_auth}} |
商户权限校验 |
基础请求 + 断言
# 请求
POST {{base_url}}/api/v2/web/order/add
Content-Type: application/json
X-Token: {{x_token}}
merchant-id: {{merchant_id}}
Merchant-Id: {{merchant_id_auth}}
{
"content": {
"menuId": 1001,
"quantity": 2
}
}
# 响应断言
HTTP 200
[Asserts]
jsonpath "$.code" == 10000
jsonpath "$.msg" == "操作成功"
jsonpath "$.data" isInteger
变量捕获(请求间传递数据)
# 如果需要动态获取 Token,可以通过登录接口捕获
POST {{base_url}}/api/v2/web/auth/login
Content-Type: application/json
{
"username": "{{username}}",
"password": "{{password}}"
}
HTTP 200
[Captures]
x_token: jsonpath "$.data.token"
注意:X-Token 优先使用 env 文件中预配置的值,避免每次都调登录接口。 仅在测试登录流程本身或 Token 过期场景时,才通过登录接口动态获取。
常用断言
[Asserts]
# 状态码
status == 200
# JSON 路径
jsonpath "$.code" == 10000
jsonpath "$.data" exists
jsonpath "$.data" isCollection
jsonpath "$.data.id" isInteger
jsonpath "$.data.name" isString
jsonpath "$.data.list" count > 0
# 类型检查
jsonpath "$.data.price" isFloat
# 包含
jsonpath "$.msg" contains "成功"
# 正则
jsonpath "$.data.phone" matches "^1[3-9]\\d{9}$"
环境变量文件格式
# env/dev.env
base_url=http://192.168.97.235:58300
# 认证 Token(X-Token 请求头,配置后所有请求自动携带)
x_token=你的Token值
# 商户路由(merchant-id 请求头,用于数据源切换)
merchant_id=你的商户ID
# 商户权限(Merchant-Id 请求头,用于权限校验)
merchant_id_auth=你的商户ID
执行命令
# 运行单个测试
hurl --test --variables-file tests/hurl/env/dev.env tests/hurl/order/create-order.hurl
# 运行模块所有测试
hurl --test --variables-file tests/hurl/env/dev.env tests/hurl/order/*.hurl
# 运行全部测试 + 生成报告
hurl --test \
--variables-file tests/hurl/env/dev.env \
--report-html tests/hurl/reports \
--report-json tests/hurl/reports/report.json \
tests/hurl/**/*.hurl
# 传递动态变量(如时间戳避免唯一键冲突)
TS=$(date +%s) && hurl --test \
--variables-file tests/hurl/env/dev.env \
--variable "voucher_type_ts=$TS" \
--variable "voucher_word_ts=$TS" \
tests/hurl/finance/*.hurl
# 指定超时(毫秒)
hurl --test --connect-timeout 5000 --max-time 30000 ...
# 失败时继续执行(不中断)
hurl --test --continue-on-error ...
生成测试的流程
第一步:读取接口文档
通过 Apifox MCP 读取指定模块的接口列表:
- 接口路径、HTTP 方法
- 请求参数(Header、Body、Query)
- 响应结构(字段名、类型、示例值)
第二步:读取 Param/VO 源码(关键!)
必须读取后端源码以确保测试完整性:
- Param 类:提取所有查询条件字段,确保每个字段都有对应测试用例
- VO 类:提取所有响应字段,在断言中验证字段存在性和类型
- Mapper XML:理解 SQL 逻辑、JOIN 关系、动态条件
示例:SubjectDetailParam 有 startDate、endDate、areaId、canteenId、keyword
→ 测试用例必须覆盖:
- 基础分页查询(startDate + endDate)
- keyword 搜索(科目编码/名称)
- 区域+食堂筛选(areaId + canteenId)
- 单日查询(startDate == endDate)
- 空结果区间(验证空数组返回)
- 分页第二页
- 导出接口
- 未授权测试
第三步:准备测试数据(先查后用,禁止硬编码)
核心原则:测试数据必须从真实环境动态获取,不能硬编码不存在的 ID 或编码。
# ✅ 正确:先查询获取真实数据,再用于后续请求
# 0a. 获取真实食堂 ID
POST {{base_url}}/api/v2/alloc/canteen/page-canteen
...
[Captures]
canteen_id: jsonpath "$.data.records[0].canteenId"
area_id: jsonpath "$.data.records[0].areaId"
# 0b. 获取关联表的真实记录
POST {{base_url}}/report/finance/setting/voucher-type/page
...
[Captures]
voucher_type_id: jsonpath "$.data.records[0].id"
# ❌ 错误:硬编码不存在的引用数据
{
"content": {
"costNo": "9999", # 可能不存在
"voucherTypeId": 1 # 可能不存在
}
}
测试数据准备检查清单:
- 外键引用的 ID 通过查询接口动态获取
- 业务编码(如 costNo)使用数据库中已存在的真实记录
- 唯一键字段使用时间戳变量避免冲突(
{{xxx_ts}}) - 创建的测试数据标记 summary 含
auto-test便于识别清理 - 测试结束后有清理步骤(删除测试数据)
第四步:生成 .hurl 文件
查询条件完整覆盖(每个 Param 字段都要有测试用例):
单接口测试场景清单:
1. 基础分页查询 — 必填参数,预期 code=10000,验证分页结构+VO 所有字段
2. 合计行验证 — 如有 totalLine,验证所有合计字段存在
3. 逐个查询条件 — 每个 Param 字段单独或组合测试
4. 空结果验证 — 故意使用不匹配的条件,验证 records count == 0
5. 分页翻页 — current=2 验证翻页正确
6. 导出接口 — 验证 code=10000(异步导出返回 void)
7. 未授权测试 — 不带 X-Token,预期 HTTP 401
8. 数据清理 — 删除测试创建的数据
数据正确性验证(不只是结构存在,还要验证值合理):
# ✅ 结构验证 + 数据正确性
[Asserts]
jsonpath "$.code" == 10000
jsonpath "$.data.resultPage.records" isCollection
jsonpath "$.data.resultPage.records" count > 0
# VO 字段存在性
jsonpath "$.data.resultPage.records[0].costNo" isString
jsonpath "$.data.resultPage.records[0].costTypeName" isString # 关联字段不能为 null
jsonpath "$.data.resultPage.records[0].debitAmount" exists
# 合计行数据合理性
jsonpath "$.data.totalLine.debitAmount" exists
jsonpath "$.data.totalLine.creditAmount" exists
第五步:执行并生成报告
hurl --test \
--variables-file tests/hurl/env/dev.env \
--report-html tests/hurl/reports \
--report-json tests/hurl/reports/report.json \
--continue-on-error \
tests/hurl/{模块}/*.hurl
第六步:解析报告
读取 report.json,输出摘要:
## 测试报告摘要
| 状态 | 数量 | 占比 |
|------|------|------|
| PASS | 15 | 75% |
| FAIL | 3 | 15% |
| ERROR| 2 | 10% |
### 失败详情
| 文件 | 接口 | 失败原因 |
|------|------|---------|
| order/create-order.hurl | POST /api/v2/web/order/add | 预期 code=10000,实际 code=40001 |
| menu/query-menu.hurl | GET /api/v2/web/menu/get/1 | 预期 HTTP 200,实际 HTTP 500 |
第七步:失败项自动触发 fix-bug 流程
当测试存在失败项时,必须自动调用 /fix-bug 走标准修复流程:
测试失败 → 分析失败原因 → 分类处理:
1. 测试数据问题(非代码 Bug):
- 引用数据不存在 → 修正测试用例中的数据
- 唯一键冲突 → 添加时间戳变量
- 权限/状态限制 → 调整测试前置条件
2. 后端代码 Bug:
→ 自动调用 Skill(fix-bug) 走标准修复流程
→ 包含:排查报告 → 用户确认 → 修复代码 → 重跑测试验证
3. 接口文档与实现不一致:
→ 输出差异报告,等待用户确认以哪个为准
fix-bug 自动触发条件:
- HTTP 状态码非预期(如 500)
- 业务码非预期(如 code != 10000)
- 响应字段缺失或类型不匹配
- 关联字段为 null(如 costTypeName 为 null 说明 JOIN 失败)
- 导出数据异常(原始字段暴露、金额未转换等)
Bug 报告格式(传递给 fix-bug):
Bug 信息:
- 接口:{METHOD} {URL}
- 请求参数:{request body}
- 预期响应:{expected}
- 实际响应:{actual}
- Hurl 文件:{file path}
- 失败断言:{assertion detail}
- 关联源码:{Controller/Service/Mapper 文件路径}
LeRequest 适配
leniu 项目的 POST 请求体使用 LeRequest<T> 封装:
# ✅ 正确:使用 content 包装 + 三个必要 Header
POST {{base_url}}/api/v2/web/order/add
Content-Type: application/json
X-Token: {{x_token}}
merchant-id: {{merchant_id}}
Merchant-Id: {{merchant_id_auth}}
{
"content": {
"menuId": 1001,
"quantity": 2
}
}
# ❌ 错误:直接传业务字段 / 缺少 Header
POST {{base_url}}/api/v2/web/order/add
{
"menuId": 1001,
"quantity": 2
}
已知陷阱(Lessons Learned)
1. del_flag 约定不统一
leniu 主表用 2=正常, 1=删除,但某些设置表(如 finance_voucher_type, finance_voucher_word)使用 @TableLogic 默认值 0=正常, 1=删除。不要盲目统一,要检查每张表的实际约定。
2. 测试数据必须使用真实存在的引用数据
关联查询(JOIN)依赖引用数据存在且状态正确。例如:
cost_type表的state = 1才是有效记录costNo必须在cost_type中存在且state = 1- 否则 LEFT JOIN 后关联字段(如
costTypeName)为 null
3. 导出验证要点
导出接口是异步的(返回 void → code=10000),需要额外关注:
- VO 的
@ExcelIgnore是否正确标记了内部字段 @ExcelProperty(order=N)的顺序是否符合产品要求- 金额字段是否配置了
converter = CustomNumberConverter.class(分→元) - 枚举字段是否有对应的描述字段(如 submitStatus → submitStatusDesc)
4. 唯一键冲突
CRUD 生命周期测试中,新增记录的唯一键字段(如 typeCode)需要加时间戳变量:
"typeCode": "AT_HURL_{{voucher_type_ts}}"
执行时通过 --variable "voucher_type_ts=$(date +%s)" 传入。
5. 后端未重启
修改后端代码后必须重启服务才能生效。如果测试结果不符合预期,先确认后端是否已重启。
.gitignore 配置
# Hurl 测试报告
tests/hurl/reports/
注意
- 测试文件(
.hurl)需要 Git 管理,报告目录不需要 env/*.env中的密码/Token 建议用环境变量替代,不要提交敏感信息- 如果是 leniu 项目的 POST 接口,请求体必须用
{"content": {...}}包装 - 生成测试前确保 Hurl CLI 已安装(
hurl --version) - 与
test-development技能的区别:本技能是真实 HTTP 请求的集成测试,test-development是 JUnit5 单元测试/MockMvc 测试