XAT:并发执行后,怎样得到可信的测试结果
从 worker 崩溃和结果缺失出发,解释任务发现、进程调度、fixture、方法时限与报告核对。
XAT 是基于 unittest 和 requests 的 Python 接口测试工具,用来运行用例、控制并发、收集日志和生成报告。我确定功能范围与执行方式,与 AI 协作实现并评审代码。它延续我在支付测试开发中接触过的问题,但将公司相关的认证、数据准备和业务断言留给接入方。
假设同一批用例中,一条通过了,一条断言失败,还有一条所在的进程直接退出。执行器最后给出的“失败”两个字,显然不足以指导排查:哪个方法跑完了,哪个结果没有回来,已经取得的日志还在不在?这篇文章沿着这批任务从发现到汇总的过程,解释 XAT 怎样保留这些信息。
运行前提与最小验证
本文核对版本为 0.6.0(d8a6f672),要求 Python ≥3.11、requests ≥2.32。需要先取得 XAT 源码访问权限;它不是本站提供的公共在线运行器,也不假定 PyPI 上的同名包就是本项目。在获授权的源码目录创建虚拟环境后运行:
python -m pip install -e .
xatrun test_report_example.py -c 2 --json-report result.json
test_report_example.py 直接复用结果报告专题的三方法例子,无需业务服务器。输入包含正常断言、断言后 cleanup 异常和 worker 直接退出;预期输出分别为 SUCCESS、SCRIPT_ERROR、SYSTEM_ERROR,批次 complete=false,命令退出 1。这个失败是预期验证结果,不表示安装失败。该专题继续解释如何读取批次 issues;单纯调用 unittest 的清理例子见资源清理。
什么时候需要这一层执行工具
少量独立用例直接使用 unittest 就能完成测试。增加一层运行器的理由,是需要统一安排多个方法的执行,并在某个任务失去响应后继续收集其他任务的结果。
例如,一个文件里有两个方法:A 正常完成,B 在执行过程中调用 os._exit(7)。B 来不及经过 unittest 的异常收集,也不会返回正常报告。若只统计收到的报告,这一批看起来只有 A,甚至可能显示“全部通过”。若只保留进程退出码,又会丢掉 A 已经完成的事实。
要发现 B 的缺失,执行器必须事先知道这一批应该包含 A 和 B,再把返回结果逐项对上。这个故障场景也说明,增加并发后应继续按任务身份核对,不能靠日志先后顺序猜测谁完成了。
先冻结任务,再启动执行
XAT 的默认执行单位是一个 test_* 方法。发现阶段由独立进程导入文件,生成方法清单;父进程核对清单中的文件与方法身份,再把任务放入队列。发现错误的文件不会拿着半份清单继续执行。
下面的图聚焦默认方法模式。worker 是运行用例的 Python 子进程;父进程中的线程池只负责启动和等待这些进程。
xatrun tests/orders tests/users -c 4 --case-timeout 600 --json-report result.json
-c 4 限制同时运行的 worker 数,父进程在一个任务结束、输出其日志后再补入下一个任务。用例代码在新解释器里执行,模块变量和请求 Session 不会直接跨方法共享;并发日志按任务收集后输出,减少不同方法的阶段信息交错。
发现本身也需要导入模块,所以顶层代码可能在发现与执行时重复运行。创建订单、修改数据库等动作应放进受生命周期管理的 fixture(测试前后的准备与清理),不能依赖模块“只导入一次”。
进程隔离改变了 fixture 的作用域
新解释器可以隔开进程内状态,也让父进程能够终止失去响应的任务。代价是每个方法都要支付解释器启动、导入和 fixture 准备成本。同一个文件的类级、模块级 fixture,在方法模式下会随各方法重复运行。
| 方式 | 一个进程承担什么 | 适用条件与代价 |
|---|---|---|
| 默认方法模式 | 一个测试方法及其 fixture | 方法能独立准备数据;文件内也能并发,但共享准备被重复执行 |
--parallel-scope file | 一个文件,文件内串行 | 保留文件内共享 fixture;某个方法挂住会影响同文件后续方法 |
这里没有做两种模式的性能对比,因此不把“更多进程”写成速度提升。选择取决于用例的数据依赖和准备成本。两个进程仍可能使用同一个账号、修改同一条记录,隔离解释器不会隔离外部业务资源。
方法发现、调度与计时的细节见执行单位与 fixture 作用域。
方法失败和执行信息缺失分别记录
一个返回 HTTP 400 的负向用例,只要符合断言,就可以成功;一个返回 HTTP 200 的请求也可能让业务断言失败。XAT 沿用 unittest 对断言和异常的判定,HTTP 状态本身不决定测试结论。
回到同一批任务,方法报告先说明每条用例是成功、断言失败、脚本错误、系统错误还是方法超时。接着,整批的 complete 回答结果有没有完整收齐;最后,exit_code 让命令行调用方决定是否继续后续步骤。这样,一条断言失败不会和一份丢失的报告混成同一件事。
| 发生什么 | 方法记录 | 整批解释 |
|---|---|---|
| A 通过,B 断言失败,均正常返回 | A 成功,B 断言失败 | complete=true,退出码 1:结论已收齐,但测试未通过 |
| A 通过,B worker 提前退出 | A 成功,B 系统错误并标记中断 | complete=false,退出码 1:B 的正常执行结果缺失 |
| A 通过,B 被父进程判定超时 | A 成功,B 超时 | complete=false,退出码 1:B 被终止,清理不保证完成 |
父进程不能仅以“读到了 JSON”作为报告有效的依据。它还核对 schema、文件身份、方法身份、收集数量,以及报告退出码与进程退出码是否一致。方法在发现后消失、报告被改坏,都不能当作成功或者悄悄从统计中略过。
完整也不等于每个方法都实际运行:类 fixture 失败可能阻止方法开始,但失败本身仍能被完整报告。skip 和 expectedFailure 也有单独记录,不能凑进成功方法数。具体例子见结果状态与完整性。
超时后的进程与资源怎样处理
HTTP 请求超时与方法执行时限是两种事件。前者是业务代码可能处理的异常;后者需要执行器在业务代码没有主动返回时采取行动。
worker 将当前方法身份、开始时间和已完成结果写入检查点。父进程轮询检查点,超过方法时限后再次读取,确认活跃方法没有变化,再终止进程。复读的原因是文件模式可能刚好切换到下一个方法,旧的开始时间不能用于杀死新方法。正常退出后还会检查实际耗时,覆盖两次轮询之间刚好结束的情况。
检查点用临时文件写完后替换,并且始终标为未完成;它只提供恢复判断所需的进度,不能提前充当成功报告。方法模式的时限覆盖 suite 中的类/模块 fixture、setup、测试、teardown 和 cleanup,不包含排队、发现和解释器启动。它也不是整次命令的全局时限:导入或发现阶段卡住,当前方法时限不能兜底。
addCleanup 可以处理正常 Python 生命周期中的异常,但进程被强制终止后,Python 清理代码无法继续执行。外部订单、账号和锁仍需由业务提供回收方式。相关机制与可运行示例见资源创建与清理顺序。
用故障检查验证这些判断
本页对应 XAT 的 d8a6f672 版本。复核使用真实子进程测试,覆盖下面几类观察;它们验证执行行为,不衡量生产吞吐或业务缺陷发现率。
| 验证输入 | 检查的结果 | 对应测试 |
|---|---|---|
| 一个方法直接退出,另一个正常完成 | 保留成功记录;退出方法有身份与系统错误;整批未完成 | test_method_scheduling.py |
| 断言失败后 cleanup 再抛异常 | 同时保留两个原因,主状态为脚本错误 | test_results.py |
| CPU 循环且捕获异常 | 父进程终止它;方法模式下后续任务继续;cleanup 未执行 | test_deadline.py |
| 修改报告版本或方法身份 | 拒绝合并不匹配的报告,产生执行错误 | test_results.py、test_method_scheduling.py |
这几类问题也会出现在远程执行器里,但当前实现仍是本机子进程。租约、网络分区后的任务归属、跨主机重试与持久化恢复尚不在这套机制的保证范围内。将来接入远程执行时,可以沿用任务身份和结果核对的思路,需要重新设计的是失联后的所有权与副作用处理。