AutoPilot Test V1.1 工程化治理系列
前两篇分别写了数据库迁移问题和执行状态问题。
到了这一篇,我开始问一个更绕的问题:
如果 AutoPilot 已经有 1109 个通过的测试,为什么我还是不敢直接说“它已经没问题了”?
答案其实很简单:
测试体系本身,也可能存在 Bug。
测试可以通过,但测试之间可能互相污染;Mock 可以全绿,但真实浏览器未必真的能跑;并发场景如果没有专门验证,生产代码一样可能存在竞态。
这一篇,就是我在 V1.1 收口阶段做的最后一轮测试工程化排查。
一、1109 passed,为什么我没有立刻庆祝?
AutoPilot 的后端测试按几个层次组织:
unit services routers integration项目里的测试大量使用 SQLite,并通过 Mock 隔离 LLM、Playwright、Appium、文件系统等外部依赖。
在这一轮 V1.1 收口的完整回归记录里,结果是:
1109 passed 2 skipped同时还有:
compileall:OK Real Chromium + Mock LLM 黄金路径:通过数字确实很好看。
但我没有把它理解成:
“1109 个测试通过,所以系统没有 Bug。”
我更愿意把它理解成:
1109 个已验证的断言通过。
这两句话差别很大。
二、第一个问题:为什么测试单独跑 PASS,组合跑却可能 FAIL?
这是测试工程里很危险的一类问题。
理想状态应该是:
Test A ↓ 不会改变 ↓ Test B 看到的环境但在 AutoPilot 的测试体系里,我碰到了一个很典型的场景。
部分模型测试会执行:
PRAGMA foreign_keys=ON而测试数据库使用共享连接模型。
测试结束以后虽然做了:
rollback但PRAGMA属于连接级状态,并不会因为事务回滚自动恢复。
于是可能出现:
Test A ↓ 打开 foreign_keys ↓ rollback ↓ 连接级 PRAGMA 还在 ↓ Test B ↓ 外键行为发生变化 ↓ FAIL这里最容易出现的误区是:
“都 rollback 了,怎么还会污染?”
答案就是:
事务状态和连接级状态不是一回事。
三、最终怎么处理?让公共 Fixture 对环境负责
我没有让每个测试自己记得写:
PRAGMA foreign_keys=OFF而是把清理动作放进公共db_sessionfixture 的 teardown。
最终逻辑是:
测试开始 ↓ 创建连接 / Session ↓ 执行测试 ↓ rollback ↓ 恢复连接级状态 ↓ 关闭连接测试 Fixture 中最终有类似:
try:connection.execute(text("PRAGMA foreign_keys = OFF"))exceptException:pass这个改动看起来很小,但它带来的工程原则很重要:
测试可以修改共享环境,但必须负责把环境还原。
四、第二个测试隔离问题:数据库迁移居然还能影响日志环境
这次排查里我特别喜欢的一点,是它让我意识到:
测试隔离绝对不只是数据库回滚。
项目的 Alembic 环境会使用:
fileConfig(...)而 migration 在 pytest 进程里运行时,可能修改当前进程里的 logging 状态。
于是可能出现:
Test A ↓ 执行 Alembic ↓ logger 配置被修改 ↓ Test B ↓ pytest 的日志捕获行为发生变化本来只是一个数据库迁移测试,最后却影响了后面所有测试看到的日志环境。
五、这个问题最后怎么处理?
我用了两层防线。
第一层,在 Alembic 环境里显式设置:
fileConfig(config.config_file_name,disable_existing_loggers=False,)它的意义是避免 migration 配置把既有 logger 简单粗暴地全部禁用。
第二层,是在相关测试里做 logging 状态快照和恢复,包括:
root logger logger level handlers disabled propagate这样即使测试过程改变了 logging 状态,测试结束以后也会恢复现场。
这个问题最后给我的结论非常简单:
只要测试改了环境,就应该由测试负责恢复环境。
六、第三个问题:Mock 测试全部正常,真实 Chromium 却炸了
前一轮我已经遇到过一次这个问题,所以这次把它正式纳入“真实黄金路径”验证。
AutoPilot 的SafePlaywright会限制 AI 生成代码访问底层 Playwright 对象。
设计目标是:
AI 代码 ↓ 白名单 API ↓ 不能直接拿到底层 page ↓ 不能通过私有属性轻易绕过限制问题在于,Playwright 自己在真实 API 调用时,也会进行调用栈分析。
实际内部会读取:
frame.f_locals["self"].__class__.__name__而原来的SafePlaywright为了安全,把__class__也拦截了。
于是:
Mock ↓ 全部通过 Real Chromium ↓ Playwright 真实内部逻辑 ↓ 访问 __class__ ↓ 被 SafePlaywright 拦截 ↓ 真实执行失败这就是为什么我越来越不愿意把 Mock 全绿直接当成真实环境全绿。
七、最终怎么修?安全边界不能以破坏底层框架为代价
这次没有直接把:
__class__全部放开。
而是提供了一个极小能力的 class proxy:
_SAFE_CLASS_PROXY=type("SafePlaywright",(),{"__name__":"SafePlaywright"},)当运行时请求__class__时,只返回这个受限对象。
也就是说:
Playwright ↓ 能拿到自己需要的 __name__但:
AI 代码 ↓ safe.__class__ ↓ 仍然被 AST Validator 拦截最后形成:
运行时兼容性 + AI 代码静态限制两层防线。
八、第四个问题:报告生成还有并发竞态
前面的测试更多关注“正确性”和“隔离”。
继续往下看,我又发现报告生成其实也可能发生竞态。
AutoPilot 的 Report 既可能被编排器后台流程触发,也可能被人工接口触发。
如果两个调用同时对同一个execution_id执行:
查询报告是否存在 ↓ 发现不存在 ↓ 生成 HTML ↓ INSERT两个线程都可能判断:
不存在然后一起 INSERT。
最终就可能撞上数据库唯一约束。
这种 Bug 单线程测试很难稳定复现。
所以在这轮治理里增加了进程内互斥:
_REPORT_LOCK=threading.Lock()生成入口使用:
with_REPORT_LOCK:returnself._generate(execution_id)并补了并发测试,用来确认同一个进程内不会同时进入这段关键区。
九、但这里还有一个重要边界:进程内锁不是分布式锁
threading.Lock()解决的是:
一个 Python 进程 + 多个线程它不能天然解决:
Worker 1 Worker 2或者:
多个进程 多个容器 多副本部署之间的竞态。
所以当前方案更准确的表达应该是:
V1.1 通过进程内互斥控制当前进程内的并发,同时依赖数据库唯一约束作为最终边界。
如果未来扩展到多副本,再考虑:
幂等写入 + 数据库唯一约束 + execution 粒度锁 / 分布式锁这个边界必须说清楚。
十、第五个问题:ORM 和数据库 Schema 也可能“各说各话”
还有一种问题特别容易被忽略:
代码里的 Model、初始化 Schema 和未来 Migration,不一定天然一致。
如果:
schema.sql ≠ ORM Model ≠ Alembic 预期结构那么短期业务可能完全不报错。
但未来一旦开始做 schema diff,就会不断出现:
“这个索引为什么又想创建一次?”所以这轮也把已经存在于数据库结构中的部分索引补回 ORM 定义,并用 autogenerate 检查结构差异。
这类问题看起来不性感,但对长期维护非常重要。
因为:
数据库结构最怕的不是复杂,而是多个地方各自维护一份“差不多”的真相。
十一、到了最后,我才真正开始做“风险登记”
做到后面,我开始意识到一个比 Bug 更危险的问题:
我很容易忘记,到底哪些东西真的验证过,哪些东西只是“理论上应该可以”。
所以最后不再追求一句:
“所有问题都解决了。”
而是明确区分:
已修复 已覆盖 环境缺失 / 未验证 已接受 / 设计边界例如真实环境类项目,当前仍应该明确区分:
Docker 实机构建与联调 Real LLM 全链路 MySQL 实机迁移 前端生产 build 多副本并发能验证的就验证。
没有环境的就明确写“未验证”。
已经知道但当前版本暂时不解决的,就记成“设计边界”。
这比一句:
“生产级。”
更有价值。
十二、1109 个测试全绿,到底证明了什么?
这一轮 V1.1 收口时记录的完整回归结果是:
1109 passed 2 skipped同时:
Real Chromium + Mock LLM 黄金路径:通过 compileall:OK这可以证明:
当前验证范围内的大量断言通过 真实 Chromium 黄金路径跑通但它不能直接推出:
系统已经没有 Bug更不能直接推出:
已经全面生产级因为仍然存在明确的验证边界。
所以我更愿意把测试结果理解成:
一组证据。
而不是:
一个结论。
十三、这轮收口以后,我对“测试开发”的理解又变了一点
以前我更容易关注:
功能有没有通过? 覆盖率多少? 测试数量多少?现在我会继续往后追:
测试会不会污染测试? Mock 通过以后真实环境呢? 并发情况下还成立吗? 状态是不是只有一个解释? 数据库模型和实际 schema 一致吗? 失败以后能不能恢复? 没有验证的风险有没有被记录?到了这里,我觉得测试开发真正做的已经不只是“写测试”。
而是在做:
建立一个能够被验证、被追溯、被解释的工程系统。
十四、结尾:1109 个测试全绿,只是一个起点
如果要把 AutoPilot V1.1 这一轮工程治理浓缩成一句话,我现在会这样说:
不是把 Bug 藏起来,而是把问题、边界和证据都摆到台面上。
一个真正可靠的系统,不应该只是:
“我觉得它应该没问题。”而应该能够回答:
这里已经验证 这里已经修复 这里有自动化测试 这里有真实环境验证 这里因为环境限制暂时没验证 这里是当前版本接受的设计边界当一个系统开始能清楚回答这些问题时,我觉得它才真正开始具备工程系统的样子。
而 AutoPilot 的 V1.1,只是这条路上的一个阶段。
系列文章
上一篇:《P0-P3 落地实录(二):进度 100% 却还在自愈,AI 测试平台的数据为什么会“自己打自己脸”?》
本文:《P0-P3 落地实录(三):从测试隔离到并发竞态,1109 个测试全绿以后我又发现了什么?》
下一篇:《我把一个 AI 编程 Prompt 推翻了很多轮,最后才明白:Prompt 不是让 AI 写代码,而是防止它绕过架构》
项目地址
GitHub:https://github.com/ZipUp-dot/AutoPilot-Test
Gitee:https://gitee.com/Mr-6Lawrence/auto-pilot-test
关于作者
我是 EthanPeng,正在做 AutoPilot。
我一直比较相信一件事:
工具应该适应人,而不是人适应工具。
AutoPilot 想做的事情也很简单:让测试人员不需要把大量时间花在重复编写、维护自动化脚本上,而是把更多精力放到测试设计、风险识别、质量保障和 AI 代码解释上。
而这次测试工程化的过程也让我越来越确定:
AI 可以帮你生成代码,但真正决定一个工具能不能进入真实工程环境的,永远是代码之外的那一整套工程能力。
欢迎交流。