用 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

代码的工程解读

  1. 为什么要这么写? 在遍历 paths 时,必须显式限制 HTTP 方法白名单({"get", "post", "put", "delete", "patch"})。因为很多工具生成的 Swagger JSON 里,paths 下除了方法名,还会混入同级的 parameters 或 $ref 字段。如果不做方法校验直接读取,下游很容易报 AttributeError。
  2. 返回值在下游怎么用? 这个函数输出的列表是纯粹的数据结构(IR)。它抹平了 Swagger 2.0 和 OpenAPI 3.0 在不同框架下的结构差异。下游无论是做接口统计透视、计算模型提示词预算,还是让大模型根据字段生成用例,都可以统一读取 full_path 和 method,不再需要关心原始文档的各种深层嵌套。
  3. 遇到问题时怎么用它排查? 如果遇到某个接口在控制台清单里显示缺失,可以先检查原始 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 预算,让大模型在可控的成本范围内,开始为这批接口设计针对性的测试用例。

继续阅读

半開

十年互联网经验,做过软件测试、Python 开发和产品工作,现在做个人产品,也写开发过程。

关于我订阅 RSS

回到文章列表