系列方向: 从大语言模型基础调用开始,逐步拆解 Prompt、RAG、规则校验、用例质量评估和自动化执行,最终整合成一个可以运行、可以验证的 AI Test Demo。
本篇重点: 将结构化测试用例转化为 pytest API 自动化测试数据,完成 HTTP 请求、参数映射和结果断言。重点不是让 AI 随意生成 Python 代码,而是让 AI 输出的数据进入一套可控的自动化执行框架。
一、测试用例生成出来了,为什么还不能直接执行?
先回顾一下前面的成果。
我们已经有这样一条流程:
text
需求文档
|
v
RAG 检索业务规则
|
v
LLM 生成测试用例
|
v
规则校验
|
v
质量评估
|
v
生成结构化 JSON
例如,AI 生成了一条创建订单的测试用例:
json
{
"id": "TC001",
"title": "未登录用户创建订单",
"rule_ids": ["R001"],
"steps": [
"提交创建订单请求"
],
"expected_result": "接口拒绝创建订单",
"expected_behavior": "defined"
}
这条数据可以阅读,也可以保存,但它还不能直接被 pytest 执行。
原因很简单:
steps 只是自然语言描述,pytest 并不知道应该请求哪个接口、传什么参数、使用什么请求头,也不知道应该检查 HTTP 状态码还是业务错误码。
如果直接让大模型生成完整 Python 自动化脚本,会出现另外一类问题:
- 不同用例生成的代码结构不一致。
- 请求地址和参数可能被模型编造。
- 断言逻辑可能不符合真实接口契约。
- 模型可能生成不安全的代码。
- 后续想统一维护请求逻辑会比较困难。
所以这次我选择另一种方式:
让 AI 生成结构化测试数据,自动化框架负责执行。
这样模型负责描述测试意图,pytest 负责真正执行测试。
二、先定义一份能够执行的测试用例结构
我们需要对前面使用的 JSON 格式做一次扩展。
之前的用例主要用于表达业务测试意图,现在需要补充接口执行信息。
例如:
json
{
"id": "TC001",
"title": "未登录用户创建订单",
"rule_ids": ["R001"],
"request": {
"method": "POST",
"path": "/api/orders",
"headers": {},
"body": {
"product_id": 1001,
"quantity": 1
}
},
"expected": {
"status_code": 401,
"json": {
"code": "UNAUTHORIZED"
}
}
}
这里需要特别说明:
/api/orders、HTTP 401 和 UNAUTHORIZED 都只是本 Demo 的接口契约示例,不代表任何真实项目的实际定义。
真实项目中,接口路径、认证机制、业务错误码都应该来自 API 文档或经过确认的接口契约。
为了让后面的自动化执行更清晰,我们把用例拆成三个部分:
| 部分 | 内容 | 作用 |
|---|---|---|
| 测试信息 | id、title、rule_ids | 记录测试目的 |
| 请求信息 | method、path、headers、body | 描述如何调用接口 |
| 预期信息 | status_code、json | 描述如何判断结果 |
这样一条用例就不再只是给人看的文字,而是可以被程序读取的测试数据。
三、搭建 pytest 自动化执行模块
在前面的项目基础上增加几个目录:
text
llm-testing-demo/
│
├── automation/
│ ├── __init__.py
│ ├── api_client.py
│ ├── test_runner.py
│ └── test_generated_cases.py
│
├── examples/
│ └── executable_cases.json
│
├── outputs/
│
├── pytest.ini
└── requirements.txt
这几个文件分别负责:
api_client.py:统一封装 HTTP 请求。test_runner.py:读取用例并执行请求和断言。test_generated_cases.py:pytest 测试入口。executable_cases.json:存放已经审核通过的可执行用例。
为什么要单独增加一个 executable_cases.json?
因为前面模型生成的候选用例,不一定都能执行。
例如,某条用例的预期结果还依赖未确认的幂等策略,那么它就不应该直接进入自动化执行阶段。
我们应该把候选用例和审核通过的可执行用例区分开。
四、第一步:准备接口测试数据
创建 examples/executable_cases.json:
json
[
{
"id": "TC001",
"title": "未登录用户创建订单",
"request": {
"method": "POST",
"path": "/api/orders",
"headers": {},
"body": {
"product_id": 1001,
"quantity": 1
}
},
"expected": {
"status_code": 401,
"json": {
"code": "UNAUTHORIZED"
}
}
},
{
"id": "TC002",
"title": "购买数量为0时创建订单",
"request": {
"method": "POST",
"path": "/api/orders",
"headers": {
"Authorization": "Bearer test-token"
},
"body": {
"product_id": 1001,
"quantity": 0
}
},
"expected": {
"status_code": 400,
"json": {
"code": "INVALID_QUANTITY"
}
}
}
]
这里设计了两条测试用例:
- TC001:验证未登录用户不能创建订单。
- TC002:验证购买数量为 0 时不能创建订单。
注意,第二条用例使用了测试环境中的示例 Token。真实项目中应该通过认证 Fixture 获取测试凭证,而不是把真实 Token 写进文件。
另外,接口响应中的 code 字段也是示例约定,实际项目需要根据接口返回结构调整。
五、第二步:封装统一的 API Client
如果每条测试用例都自己调用 requests.post(),后续维护会比较麻烦。
例如,接口超时、请求日志、基础地址变化、公共请求头等,都可能需要修改很多地方。
所以先统一封装一个客户端。
安装依赖:
bash
pip install pytest requests
创建 automation/api_client.py:
python
import requests
class APIClient:
def __init__(self, base_url, timeout=10):
self.base_url = base_url.rstrip("/")
self.timeout = timeout
def request(
self,
method,
path,
headers=None,
body=None
):
url = self.base_url + path
response = requests.request(
method=method,
url=url,
headers=headers or {},
json=body,
timeout=self.timeout
)
return response
这里没有把所有请求方法分别写成 get()、post()、put()。
因为当前 Demo 只需要一个统一请求入口,后续如果需要增加认证、重试或日志记录,可以在这个类中集中处理。
但要注意:生产环境不应该对所有请求盲目重试。特别是创建订单这种可能产生副作用的 POST 请求,重试必须考虑幂等性。
六、第三步:编写测试用例执行器
接下来需要解决一个问题:
JSON 中的测试数据,如何映射成真正的 HTTP 请求和断言?
创建 automation/test_runner.py:
python
def execute_case(client, case):
request_data = case["request"]
expected = case["expected"]
response = client.request(
method=request_data["method"],
path=request_data["path"],
headers=request_data.get("headers"),
body=request_data.get("body")
)
# 检查 HTTP 状态码
assert response.status_code == expected["status_code"], (
f"{case['id']} 状态码不符合预期,"
f"实际:{response.status_code},"
f"预期:{expected['status_code']},"
f"响应:{response.text}"
)
# 检查 JSON 业务字段
actual_json = response.json()
for key, expected_value in expected.get("json", {}).items():
assert actual_json.get(key) == expected_value, (
f"{case['id']} 字段 {key} 不符合预期,"
f"实际:{actual_json.get(key)},"
f"预期:{expected_value}"
)
这里的执行逻辑非常直接:
- 从用例中获取请求方法。
- 获取接口路径。
- 读取请求头和请求体。
- 发送 HTTP 请求。
- 检查 HTTP 状态码。
- 检查 JSON 中指定的业务字段。
这里还有一个值得注意的地方:
我们没有让 AI 生成断言代码,而是让它输出预期数据,由固定的执行器完成断言。
这样做的好处是,测试框架的执行逻辑保持一致,不会因为模型生成方式不同而发生变化。
当前实现只支持简单的 JSON 顶层字段比较。后续如果需要检查嵌套字段、数组、数据库状态或响应时间,可以逐步扩展断言机制。
七、第四步:接入 pytest
现在已经有了 API Client 和执行器,接下来只需要让 pytest 读取测试用例。
创建 automation/test_generated_cases.py:
python
import json
import os
from pathlib import Path
import pytest
from automation.api_client import APIClient
from automation.test_runner import execute_case
CASE_FILE = Path("examples/executable_cases.json")
def load_cases():
with CASE_FILE.open("r", encoding="utf-8") as file:
return json.load(file)
@pytest.fixture(scope="session")
def api_client():
base_url = os.getenv("TEST_BASE_URL")
if not base_url:
pytest.fail("未配置 TEST_BASE_URL")
return APIClient(base_url=base_url)
def load_case_ids():
return [
case["id"]
for case in load_cases()
]
@pytest.mark.parametrize(
"case",
load_cases(),
ids=load_case_ids()
)
def test_generated_case(api_client, case):
execute_case(api_client, case)
这里使用了 pytest 的 parametrize。
它的作用是将 JSON 中的多条测试用例展开成独立的测试项。
例如,原本只有一个测试函数:
python
def test_generated_case():
pass
经过参数化之后,pytest 可以分别执行 TC001 和 TC002。
这样有几个好处:
- 每条用例有独立执行结果。
- 失败时可以定位具体用例。
- 可以使用 pytest 的测试报告能力。
- 后续可以把 AI 生成的用例数量动态扩展。
这里有一个工程细节:parametrize 在测试收集阶段就会读取 JSON,因此文件不存在或格式错误时,pytest 可能在测试执行前就失败。实际项目中应该提前校验用例文件,并给出更清晰的错误提示。
八、第五步:运行自动化测试
创建 pytest.ini:
ini
[pytest]
testpaths = automation
python_files = test_*.py
addopts = -v
然后配置测试环境地址:
bash
export TEST_BASE_URL="http://127.0.0.1:8000"
Windows PowerShell:
powershell
$env:TEST_BASE_URL="http://127.0.0.1:8000"
运行:
bash
pytest
如果接口服务已经启动,并且接口契约与示例用例一致,pytest 就会分别执行两条测试。
预期测试项:
text
TC001 未登录用户创建订单
TC002 购买数量为0时创建订单
这只是预期的测试项名称,不是实际运行结果。
如果没有真实订单服务,这两条用例不会凭空通过。我们需要先准备一个符合接口契约的测试环境,或者编写 Mock 服务验证执行器本身。
九、没有真实接口,怎么先验证执行器?
我认为这里很值得做一个小实验。
因为在实际开发中,我们经常会遇到这样的情况:
自动化框架已经写好了,但后端接口还没有部署完成。
这时候可以使用 Mock 服务验证请求构造和断言逻辑。
例如,创建一个简单的 Flask 服务。
安装:
bash
pip install flask
创建 mock_server.py:
python
from flask import Flask, request, jsonify
app = Flask(__name__)
@app.route("/api/orders", methods=["POST"])
def create_order():
if not request.headers.get("Authorization"):
return jsonify({
"code": "UNAUTHORIZED"
}), 401
data = request.get_json(silent=True) or {}
quantity = data.get("quantity")
if (
not isinstance(quantity, int)
or isinstance(quantity, bool)
or quantity <= 0
):
return jsonify({
"code": "INVALID_QUANTITY"
}), 400
return jsonify({
"code": "SUCCESS",
"order_id": "MOCK-ORDER-001"
}), 201
if __name__ == "__main__":
app.run(
host="127.0.0.1",
port=8000,
debug=False
)
启动:
bash
python mock_server.py
这只是一个用于验证执行器的 Mock 接口。
它没有实现真实订单创建、库存扣减、数据库事务和幂等策略。
现在运行:
bash
pytest
我们可以验证:
- 未登录请求是否得到 401。
- 数量为 0 的请求是否得到 400。
- 执行器能否正确解析响应。
- pytest 能否识别每条用例的执行结果。
这里的价值不是证明订单业务正确,而是先验证 AI 输出的数据能否被自动化框架正确消费。
这是我比较推荐的开发顺序:先验证工具链,再接入真实业务系统。
十、AI 生成的用例,不能直接进入执行环境
到这里,技术链路已经比较完整了。
但我认为有一个问题必须单独强调:
模型生成的测试用例,不应该未经检查就直接执行。
特别是当测试工具开始接入真实业务环境时,风险会明显增加。
例如:
- 模型生成了一个删除数据的请求。
- 模型把测试地址错误地指向生产环境。
- 模型生成了超出预期范围的请求参数。
- 模型错误地使用了管理员身份。
- 模型生成的预期结果本身就是错误的。
因此,正式执行前至少要增加以下控制:
- 只允许访问配置好的测试环境地址。
- 对 HTTP 方法和接口路径设置白名单。
- 对请求参数进行 Schema 校验。
- 对破坏性操作设置人工审批。
- 对 Token、密码等敏感信息进行保护。
- 对超时、并发和请求数量设置限制。
- 不允许模型输出任意 Python 代码并直接执行。
尤其是最后一点。
我们现在采用的是 JSON 数据驱动模式,模型只负责输出受约束的数据,程序负责解释和执行。
这比直接运行模型生成的 Python 脚本更容易审计,也更容易控制风险。
十一、这一阶段还有哪些不足?
目前的执行框架已经能完成简单的 API 请求和断言,但离真正的企业级自动化平台还有距离。
| 当前能力 | 后续需要补充 |
|---|---|
| JSON 参数化执行 | 更完整的用例 Schema |
| HTTP 状态码断言 | 嵌套 JSON、数据库和业务状态断言 |
| 简单 Mock 服务 | 真实测试环境接入 |
| pytest 执行 | 测试报告和失败日志分析 |
| 固定测试 Token | 统一认证与凭证管理 |
| 单请求执行 | 数据清理、依赖管理和并发控制 |
还有一个容易忽略的问题:测试数据之间可能存在依赖关系。
例如,创建订单成功后,需要查询订单详情;查询订单详情又依赖前一步生成的订单 ID。
这种场景不能简单地把每条 JSON 用例独立执行,需要增加测试数据关联、前置条件和后置清理机制。
这部分可以放在后续工程化阶段,不必在第一版 Demo 中一次性完成。
十二、从生成到执行,AI Test Demo 又向前走了一步
回顾整个流程:
text
需求输入
|
v
RAG 检索业务规则
|
v
LLM 生成候选用例
|
v
规则校验
|
v
质量评估
|
v
人工确认可执行用例
|
v
JSON 数据驱动
|
v
pytest 执行
|
v
API 请求与断言
|
v
测试执行结果
这次最大的变化,是我们不再停留在"AI 能不能生成测试用例"这个问题上,而是开始验证:
- 生成的数据能不能被自动化框架消费?
- 测试请求能不能真正发出去?
- 预期结果能不能被程序判断?
- 执行失败能不能定位到具体用例?
这些问题比单纯比较模型生成了多少条用例更接近实际测试工作。
本篇小结: AI 生成测试用例的价值,不在于让它替代测试工程师写 Python,而在于把需求理解、场景设计和自动化执行连接起来。模型生成候选数据,框架执行确定性逻辑,人工把控业务正确性,这样的分工才更容易形成稳定的工程流程。