用 AI 做接口测试,先从一份 OpenAPI 开始
· 教程
让大模型写用例之前,得先让它拿到真实可靠的接口定义。记录一下在搭建测试 Agent 时,配置 OpenAPI / Swagger 数据源容易遇到的边界与处理细节。
平时聊起用大模型做接口测试,最直观的想法往往是:把 Swagger 上的 JSON 贴进对话框,让模型“针对这个接口写几个测试用例”。如果只是测试一两个简单接口,写个脚本玩玩确实能出结果。
但如果要把这个过程做成一个能重复跑的测试工具,很快就会遇到几个很现实的麻烦:
- 在线接口随时在变,今天跑的用例,下周接口改了一个字段,测试用例和当时的接口版本完全脱节;
- 文档里大量出现复杂的对象嵌套和循环
$ref引用,一股脑喂给模型不仅消耗大量 token,还容易超出上下文上限; - 内网服务通常需要特定的认证凭据,如果直接把带有真实 Token 的 URL 或配置写入任务,会带来凭据泄漏的风险。
在把接口交给模型生成用例之前,第一步其实跟模型没太大关系:必须先把接口定义处理成一份确定的、冻结的本地过程资产。
两种数据源头的权衡
要让测试 Agent 知道被测系统有哪些接口,通常有两种来源:
1. 接口文档模式(OpenAPI / Swagger)
这是通常推荐的首选路径。后端工程(如 Spring Boot 使用 Knife4j、Node.js 使用 Egg 或 Python 使用 FastAPI)通常会自动生成 Swagger 或 OpenAPI 文档。
通过解析文档直接提取接口信息,投入产出比最高:
- 它的结构天然是结构化的,包含了明确的 HTTP 方法、URL 路径、请求参数类型和状态码枚举;
- 不需要拉取被测项目的源代码,更不需要在本地搭建一套复杂的编译和运行环境。
2. 代码仓库模式(Git Repository)
如果面对的是一些老旧系统,文档年久失修甚至完全没有 Swagger,就只能退回到代码仓库模式:通过抽象语法树(AST)去静态分析后端的控制器代码(如 Spring 的 @RestController 或 FastAPI 的路由修饰器)。
这种方式虽然不依赖文档,但实现难度高很多:不仅需要处理各种跨文件的路由挂载与前缀拼接,而且静态分析很难完全推断出请求体的详细 JSON 结构。
因此,只要系统有可用的 OpenAPI 或 Swagger 端点,优先走文档模式往往是最稳妥的选择。
配置数据源时的四个细节
在界面或 CLI 中配置 OpenAPI 数据源时,看似只是填一个文件路径或者 URL,但底层有四个细节需要提前定好规矩:
1. 挂载 Base Path 的显式拼接
很多微服务导出的 Swagger 文件中,定义的路径往往只是局部路径,例如 /jobs。但在网关或者真实微服务环境中,外部访问往往带有统一前缀,比如 /api/recruit。
如果不在数据源解析阶段显式声明并挂载 Base Path,工具生成的接口清单就全变成了 /jobs。到了最后的执行发包环节,所有的请求都会因路径错误直接返回 404。在登记数据源时,把网关前缀与相对路径拼接完整,是后续能够正确调通接口的前提。
2. 文档大小与资源预算
大型系统的 Swagger JSON 动辄几兆甚至几十兆,里面包含数百个接口定义。如果直接拉取并解析,不仅下载耗时长,而且过大的文本后续根本无法放进大模型的上下文窗口里。
在适配器拉取文档时,必须设置严格的预算上限:
- 限制单份文档的最大字节数(例如默认 20MB)以及多文档聚合时的总字节数(如 100MB);
- 设置网络请求超时时间(如 15 秒),遇到超时直接报错,不假装解析成功。
3. 凭据只存变量名,不存真实值
如果在线 Swagger 端点部署在开发内网,需要通过认证头才能访问,切忌直接在任务配置文件或 URL 参数里写入明文 Token。
安全规范的做法是采用“环境变量映射”:配置文件里只记录环境变量的名称(例如 Authorization = "TESTPILOT_DOC_AUTH")。发起网络抓取时,程序从本机当前的环境变量里读取真实的值拼接请求头。这样生成的任务配置文件无论怎么流转,都不会携带任何真实凭据。
4. 显式标记为“未核验证档”(unverified-document)
接口文档本质上是人写的说明或者注解生成的描述,不能直接等同于“正在运行的服务行为”。文档里写了某个字段是必填,代码里可能根本没做校验;文档里写了返回 200,线上可能早已废弃。
因此,从文档解析出的接口清单在元数据中必须带上 unverified-document 标签。这提醒后续查看报告的人:测试是基于这份文档契约生成的,文档本身是否与生产环境 100% 保持一致,需要结合真实执行证据另行判断。
简单的接口发现与解析代码
在本地实现一个轻量的 OpenAPI 契约解析器,并不需要复杂的外部框架。使用 Python 标准库解析本地或在线导出的 Swagger JSON,提取核心接口元数据:
import json
import re
from typing import Any, Dict, List
def parse_openapi_spec(
spec_data: Dict[str, Any], base_path: str = ""
) -> List[Dict[str, Any]]:
"""解析 OpenAPI / Swagger 原始字典,提取标准化的接口列表。"""
normalized_interfaces = []
paths = spec_data.get("paths", {})
# 清理并规范化 Base Path,确保以 / 开头且不以 / 结尾
clean_base = "/" + base_path.strip("/") if base_path.strip("/") else ""
# 遍历所有定义的路由路径与 HTTP 方法
for route_path, methods in paths.items():
# 拼接网关前缀与路由相对路径
full_path = f"{clean_base}/{route_path.lstrip('/')}"
for method, operation in methods.items():
# 过滤非标准 HTTP 方法字段(如 summary、parameters 等全局定义)
if method.lower() not in {"get", "post", "put", "delete", "patch"}:
continue
summary = operation.get("summary", "")
operation_id = operation.get("operationId", "")
# 提取参数列表,记录参数名称、位置与是否必填
param_list = []
for p in operation.get("parameters", []):
param_list.append(
{
"name": p.get("name"),
"in": p.get("in"), # query, path, header, body
"required": p.get("required", False),
}
)
# 提取请求体引用或 Schema 类型(兼容 OpenAPI 3.x 与 Swagger 2.0)
has_body = False
if method.lower() in {"post", "put", "patch"}:
if "requestBody" in operation or any(
p.get("in") == "body" for p in operation.get("parameters", [])
):
has_body = True
normalized_interfaces.append(
{
"method": method.upper(),
"full_path": full_path,
"summary": summary,
"operation_id": operation_id,
"has_body": has_body,
"param_count": len(param_list),
"document_status": "unverified-document",
}
)
return normalized_interfaces
代码的工程解读
- 为什么要这么写?
在遍历 paths 时,必须显式限制 HTTP 方法白名单(
{"get", "post", "put", "delete", "patch"})。因为很多工具生成的 Swagger JSON 里,paths下除了方法名,还会混入同级的parameters或$ref字段。如果不做方法校验直接读取,下游很容易报 AttributeError。 - 返回值在下游怎么用?
这个函数输出的列表是纯粹的数据结构(IR)。它抹平了 Swagger 2.0 和 OpenAPI 3.0 在不同框架下的结构差异。下游无论是做接口统计透视、计算模型提示词预算,还是让大模型根据字段生成用例,都可以统一读取
full_path和method,不再需要关心原始文档的各种深层嵌套。 - 遇到问题时怎么用它排查?
如果遇到某个接口在控制台清单里显示缺失,可以先检查原始 JSON 的
paths中是否含有该 URL;再看对应的方法名是否全小写;最后检查拼接后的full_path是否符合预期,避免因前缀双斜杠(//)导致匹配异常。
任务配置原件:task.json
当我们在控制台选择完数据源并点击“执行接口解析”后,系统会为这次任务生成一份只读的配置文件 task.json,存放在本地任务目录(如 runtime/tasks/task-apple-01/task.json)中:
{
"task_id": "task-apple-01",
"target_project": "APPLE-recruitService",
"adapter": "openapi",
"document_paths": [
"targets/apple/recruit-openapi.json"
],
"version_manifest": {},
"scope": [
"*"
],
"status": "discovered"
}
这份配置文件锁定了五个维度的契约:
task_id:当前任务的唯一代号,所有后续生成的材料、审核结果和执行证据都会绑定在这个 ID 下;adapter:声明本次数据使用的是openapi适配器,系统会使用固定的解析逻辑读取文档;document_paths:记录本次文档的来源相对路径,工具会自动计算并记录该文件的 SHA256 哈希值;scope:标记本轮测试的接口范围,"*"代表全量接口,也可以按 glob 语法指定特定模块(如/api/recruit/jobs/*);status:状态从初始的created流转为discovered。
一旦任务状态变为 discovered,当前任务的目标和源头文件就被严格锁定。后续不允许在同一个任务里偷偷替换 Swagger 文件,以保证从接口定义、模型用例设计到最后测试对账的完整可追溯。如果服务发布了新版本、换了新文档,正确的做法是新建一个独立的任务来承接。
准备好接口清单之后
完成数据源配置与解析后,第一步的职责就结束了。
在这个阶段,系统只做了一件事:把外部形态多样的接口文档,清洗并冻结成一份标准化的本地接口清单。这里没有调用任何大语言模型,也没有向被测服务发送任何真实的网络测试请求。
把数据源头和接口边界用代码稳稳锁住之后,下一步才轮到配置模型和计算 Token 预算,让大模型在可控的成本范围内,开始为这批接口设计针对性的测试用例。