1. 引言
2026 年已经过半,AI 编程早已不是「能不能用」的问题,而是「怎么用得稳、用得省」的问题。今天这篇文章,我打算用一次真实的 Python 接口自动化测试案例,完整复盘我是如何借助 ChatGPT Plus 与 Codex 的组合,把一个「靠人肉点点点」的回归测试流程,改造成「一键执行、自动断言、失败即告警」的可靠流水线。
文章会覆盖从测试方案设计、用例生成、Mock 数据构造、问题排查到最终接入 CI 的全过程,包含至少 3 段可运行的代码、一张架构流程图、一份优缺点对比表,以及一个 FAQ 小节。全程不涉及任何充值、代充或购买渠道,请放心阅读。
2. 背景:那个「每次发版都提心吊胆」的接口回归
先交代一下背景。我所在的团队维护着一个基于 Python 3.10 + FastAPI + SQLAlchemy + PostgreSQL 的订单查询服务。接口数量不算多,但每次发版前,回归测试都靠测试同学手工在 Postman 里点:
- 核心接口超过 40 个,手工回归一轮要 2 小时;
- 断言全靠肉眼比对返回 JSON,漏判时有发生;
- 没有 Mock 环境,联调依赖真实数据库,数据一脏就全乱;
- 每次发版,接口回归都是「提心吊胆」的高危环节。
改造的目标很明确:在不改变接口行为的前提下,把回归测试脚本化、自动化,补上 Mock 与断言,并让测试结果可追溯、可告警。
3. 技术选型:为什么是 ChatGPT Plus + Codex
在开始之前,先说明我这次使用的工具组合:
| 工具 | 用途 | 我的角色 |
|---|---|---|
| ChatGPT Plus | 测试方案设计、用例梳理、断言规则评审 | 提问者、决策者 |
| Codex | 批量生成测试脚本、Mock 数据、CI 配置 | 指令下达者、审查者 |
| pytest + requests + Git | 测试执行、版本控制、人工复核 | 最终把关人 |
选择这个组合的原因有三点:
- ChatGPT Plus 适合「想清楚」:它的推理能力适合做接口边界梳理、异常场景识别和断言规则设计;
- Codex 适合「写出来」:它擅长在明确指令下批量产出测试脚本和 Mock 数据,尤其是机械性的重复代码;
- 两者互补:ChatGPT Plus 负责「测什么」,Codex 负责「怎么测」,我负责「测得对不对」。
4. 第一步:用 ChatGPT Plus 做接口清单与用例梳理
改造的第一步不是写代码,而是把要测的接口和场景梳理清楚。我把服务的 OpenAPI 文档整理成一段描述,交给 ChatGPT Plus,让它帮我列出完整的测试用例矩阵。
我给出的原始描述(节选):
text
服务包含订单查询、订单创建、订单取消三个核心接口。
订单查询支持按订单号、按用户 ID、按时间范围三种方式;
订单创建需要校验商品库存与用户余额;
订单取消只允许待支付或已支付状态。请帮我梳理测试用例矩阵,并指出边界与异常场景。
ChatGPT Plus 返回的用例梳理结果如下:
#mermaid-svg-GQulxRFp6hq1uzSZ{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GQulxRFp6hq1uzSZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GQulxRFp6hq1uzSZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GQulxRFp6hq1uzSZ .error-icon{fill:#552222;}#mermaid-svg-GQulxRFp6hq1uzSZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GQulxRFp6hq1uzSZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GQulxRFp6hq1uzSZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GQulxRFp6hq1uzSZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GQulxRFp6hq1uzSZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GQulxRFp6hq1uzSZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GQulxRFp6hq1uzSZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GQulxRFp6hq1uzSZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GQulxRFp6hq1uzSZ .marker.cross{stroke:#333333;}#mermaid-svg-GQulxRFp6hq1uzSZ svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GQulxRFp6hq1uzSZ p{margin:0;}#mermaid-svg-GQulxRFp6hq1uzSZ .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GQulxRFp6hq1uzSZ .cluster-label text{fill:#333;}#mermaid-svg-GQulxRFp6hq1uzSZ .cluster-label span{color:#333;}#mermaid-svg-GQulxRFp6hq1uzSZ .cluster-label span p{background-color:transparent;}#mermaid-svg-GQulxRFp6hq1uzSZ .label text,#mermaid-svg-GQulxRFp6hq1uzSZ span{fill:#333;color:#333;}#mermaid-svg-GQulxRFp6hq1uzSZ .node rect,#mermaid-svg-GQulxRFp6hq1uzSZ .node circle,#mermaid-svg-GQulxRFp6hq1uzSZ .node ellipse,#mermaid-svg-GQulxRFp6hq1uzSZ .node polygon,#mermaid-svg-GQulxRFp6hq1uzSZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GQulxRFp6hq1uzSZ .rough-node .label text,#mermaid-svg-GQulxRFp6hq1uzSZ .node .label text,#mermaid-svg-GQulxRFp6hq1uzSZ .image-shape .label,#mermaid-svg-GQulxRFp6hq1uzSZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-GQulxRFp6hq1uzSZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GQulxRFp6hq1uzSZ .rough-node .label,#mermaid-svg-GQulxRFp6hq1uzSZ .node .label,#mermaid-svg-GQulxRFp6hq1uzSZ .image-shape .label,#mermaid-svg-GQulxRFp6hq1uzSZ .icon-shape .label{text-align:center;}#mermaid-svg-GQulxRFp6hq1uzSZ .node.clickable{cursor:pointer;}#mermaid-svg-GQulxRFp6hq1uzSZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GQulxRFp6hq1uzSZ .arrowheadPath{fill:#333333;}#mermaid-svg-GQulxRFp6hq1uzSZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GQulxRFp6hq1uzSZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GQulxRFp6hq1uzSZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GQulxRFp6hq1uzSZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GQulxRFp6hq1uzSZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GQulxRFp6hq1uzSZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GQulxRFp6hq1uzSZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GQulxRFp6hq1uzSZ .cluster text{fill:#333;}#mermaid-svg-GQulxRFp6hq1uzSZ .cluster span{color:#333;}#mermaid-svg-GQulxRFp6hq1uzSZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GQulxRFp6hq1uzSZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GQulxRFp6hq1uzSZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-GQulxRFp6hq1uzSZ .icon-shape,#mermaid-svg-GQulxRFp6hq1uzSZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GQulxRFp6hq1uzSZ .icon-shape p,#mermaid-svg-GQulxRFp6hq1uzSZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GQulxRFp6hq1uzSZ .icon-shape .label rect,#mermaid-svg-GQulxRFp6hq1uzSZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GQulxRFp6hq1uzSZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GQulxRFp6hq1uzSZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GQulxRFp6hq1uzSZ :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 接口测试用例设计
正常路径
边界场景
异常场景
按订单号查询
按用户 ID 查询
按时间范围查询
空结果集
分页边界
时间范围为空
订单不存在
库存不足
非法状态流转
这一步的价值在于:把隐性的测试盲区显性化。在梳理过程中,ChatGPT Plus 还指出了两个我原先没注意到的边界场景------「时间范围跨年」和「订单号包含前导零」,这两条在旧的手工回归里从来没人测过。
5. 第二步:用 Codex 生成接口测试脚本
用例矩阵梳理清楚之后,就轮到 Codex 上场了。我给它下达了明确的生成指令,要求产出基于 pytest + requests 的测试脚本。
第一段代码:公共请求封装与断言工具。
python
# conftest.py
import pytest
import requests
BASE_URL = "http://localhost:8000"
@pytest.fixture
def client():
session = requests.Session()
session.headers.update({"Content-Type": "application/json"})
yield session
session.close()
def assert_ok(resp):
assert resp.status_code == 200, f"HTTP {resp.status_code}: {resp.text}"
return resp.json()
def assert_error(resp, expected_code):
assert resp.status_code == expected_code, f"HTTP {resp.status_code}: {resp.text}"
第二段代码:订单查询接口的测试用例。
python
# test_order_query.py
def test_query_by_order_id(client):
resp = client.get("/orders/A001")
data = assert_ok(resp)
assert data["order_id"] == "A001"
assert data["status"] in {"PENDING_PAYMENT", "PAID", "SHIPPED"}
def test_query_by_user_id_empty(client):
resp = client.get("/orders?user_id=no_such_user")
data = assert_ok(resp)
assert data["items"] == []
assert data["total"] == 0
def test_query_by_time_range_cross_year(client):
resp = client.get("/orders?start=2025-12-31&end=2026-01-02")
data = assert_ok(resp)
assert data["total"] >= 0
第三段代码:订单创建与取消的异常场景测试。
python
# test_order_mutation.py
def test_create_order_insufficient_stock(client):
payload = {"product_id": "P001", "quantity": 99999}
resp = client.post("/orders", json=payload)
assert_error(resp, 409)
assert "insufficient_stock" in resp.json()["error"]
def test_cancel_order_invalid_state(client):
payload = {"order_id": "A003", "status": "COMPLETED"}
resp = client.post("/orders/A003/cancel", json=payload)
assert_error(resp, 400)
assert "invalid_state" in resp.json()["error"]
Codex 生成这三段代码只花了不到三分钟。但请注意:生成快不代表可以直接用。我逐行审查后发现两个问题:
- 第一段代码里
assert_ok没有处理 2xx 之外的 3xx 重定向,需要显式关闭allow_redirects; - 第二段代码的跨年查询依赖真实数据库,需要改成 Mock 数据源,否则 CI 环境跑不起来。
这两个问题说明:AI 生成代码必须经过人工审查和适配,尤其是测试环境的隔离性。
6. 第三步:用 Codex 构造 Mock 数据源
为了让测试不依赖真实数据库,我让 Codex 基于 FastAPI 的依赖注入生成一个内存版 Mock 仓储。
python
# mock_repository.py
from dataclasses import dataclass, field
@dataclass
class MockOrderRepository:
_orders: dict = field(default_factory=dict)
def seed(self, order_id: str, status: str):
self._orders[order_id] = {"order_id": order_id, "status": status}
def find_by_id(self, order_id: str):
return self._orders.get(order_id)
def find_by_user(self, user_id: str):
return [o for o in self._orders.values() if o.get("user_id") == user_id]
python
# test_main.py
from fastapi.testclient import TestClient
from main import app
from mock_repository import MockOrderRepository
repo = MockOrderRepository()
repo.seed("A001", "PAID")
repo.seed("A002", "PENDING_PAYMENT")
app.dependency_overrides[get_repository] = lambda: repo
client = TestClient(app)
def test_query_mock_order():
resp = client.get("/orders/A001")
assert resp.status_code == 200
assert resp.json()["status"] == "PAID"
Mock 数据源跑通之后,我把真实数据库的依赖彻底从测试里摘除,确保任何一台机器、任何一次 CI 都能稳定复现同样的结果。
7. 第四步:问题排查实录
改造过程中并非一帆风顺,这里记录两个典型的排查过程。
7.1 问题一:pytest 收集不到测试文件
把测试脚本放进 tests/ 目录后,运行 pytest 提示 no tests ran。排查步骤:
- 检查目录结构:确认
tests/下没有__init__.py,pytest 默认递归收集需要它; - 检查命名规则:确认文件以
test_开头、函数以test_开头; - 解决方案:在
tests/下补一个空的__init__.py,并显式指定pytest tests/。
bash
pytest tests/ -v --disable-warnings
7.2 问题二:Mock 数据在用例之间互相污染
多个测试用例共用同一个 MockOrderRepository 实例,导致前一个用例写入的数据影响后一个用例。排查步骤:
- 复现:连续运行两个用例,第二个用例断言失败;
- 定位:
repo是模块级单例,状态在用例间共享; - 解决:把
repo改成 pytest fixture,每个用例自动重建。
python
@pytest.fixture
def repo():
r = MockOrderRepository()
r.seed("A001", "PAID")
return r
8. 优缺点分析
这次「ChatGPT Plus + Codex」组合改造,我整理了一份客观的优缺点对比:
| 维度 | 优点 | 缺点 |
|---|---|---|
| 效率 | 测试脚本生成速度快,重复劳动大幅减少 | 生成结果仍需人工逐行审查 |
| 质量 | 用例矩阵梳理完整,边界场景覆盖补全 | 对测试环境隔离性不敏感 |
| 成本 | 无需额外购买工具,现有订阅即可 | 提示词设计需要学习成本 |
| 风险 | 可快速产出多种测试方案对比 | 过度信任 AI 输出会引入隐患 |
9. FAQ
Q1:ChatGPT Plus 和 Codex 有什么区别?
ChatGPT Plus 更偏向对话式推理与方案设计,适合做用例梳理、断言规则评审和知识问答;Codex 更偏向在明确指令下批量生成和修改代码,适合执行具体的编码任务。两者可以配合使用。
Q2:AI 生成的测试代码可以直接进 CI 吗?
不建议。AI 生成代码必须经过人工审查、本地运行和代码评审,尤其要注意测试环境隔离、Mock 数据独立性和断言准确性。
Q3:这次改造花了多长时间?
从用例梳理到 CI 跑通,大约用了一天半。其中用例矩阵设计和 Mock 数据构造占了大半天,脚本生成只占半天,剩余时间用于审查和修复问题。
Q4:没有 AI 编程经验的人能上手吗?
可以。建议从小的、边界清晰的接口开始练习,先学会「把测试场景讲清楚」,再逐步尝试更复杂的测试改造任务。
10. 总结
这次改造让我对 AI 编程有了更务实的认识:AI 不是替代测试工程师,而是放大测试工程师的生产力。ChatGPT Plus 帮我「想清楚」,Codex 帮我「写出来」,而我负责「把好关」。
如果你也在考虑用 AI 编程改造接口测试,我的建议是:先从用例矩阵、Mock 数据这类边界清晰的模块入手,把断言补上,把环境隔离做好,再逐步扩大范围。AI 编程的价值,不在于一次生成多少测试代码,而在于帮你把那些「每次发版都提心吊胆」的回归流程,变成「一键执行、自动断言、失败即告警」的可靠流水线。