最近用ChatGPT、Codex排查测试问题时,有一类故障特别容易把人带偏:

单个测试拿出来跑,全部通过。

但一跑整套测试:

突然失败。

更麻烦的是:

再跑一次,失败的可能还不是同一个测试。

例如:

test_create_order        PASS
test_cancel_order        PASS
test_update_inventory    PASS

单独执行全部正常。

但执行:

pytest

却变成:

test_update_inventory    FAIL

再跑一次:

test_cancel_order        FAIL

这时候很多人的第一反应是:

测试本身不稳定。

或者让Codex继续:

重跑测试。

加Retry。

调Timeout。

甚至直接修改失败断言。

但真正的问题可能根本不在这个“失败的测试”里。

而在:

Test Isolation——测试隔离

更准确一点说:

某个测试改变了其他测试运行时所依赖的环境。

所以才会出现:

单独都对。

放在一起却错。


一、为什么单个测试通过,不代表整个测试集没问题?

一个测试单独执行时,它通常面对的是:

相对干净的环境。

比如:

数据库初始状态固定。

全局变量还是默认值。

Mock还没被其他测试改过。

缓存为空。

临时文件不存在。

所以测试当然可以正常通过。

但整套测试执行时:

前一个测试留下的状态,可能会影响后一个测试。

例如测试A执行了:

user.role = "admin"

但执行结束以后没有恢复。

测试B原本假设:

user.role = "guest"

那测试B单独跑:

通过。

跟在测试A后面跑:

失败。

于是表面上看:

test_b有问题。

实际上真正污染环境的是:

test_a。

这就是测试污染最麻烦的地方。

报错位置和根因位置经常不是同一个地方。


二、最常见的第一类污染:数据库状态没有清理

比如测试A创建了一条订单:

order_id = 1001

测试结束以后,这条数据仍然留在测试数据库里。

测试B开始时假设:

订单表为空。

于是执行:

assert order_count == 0

直接失败。

但如果只运行测试B:

数据库刚好是干净的。

它自然又会通过。

这类问题非常常见于:

共享测试数据库。

Fixture清理不完整。

Transaction没有Rollback。

测试异常退出以后跳过Teardown。

尤其是Agent自动补测试时,很容易只考虑:

怎么创建测试数据。

却没有同时考虑:

测试结束后怎么恢复环境。


三、第二类污染:全局变量和Singleton状态

比如项目里存在:

config.retry_count = 3

测试A为了验证特殊情况改成:

config.retry_count = 10

但结束以后没有恢复。

测试B继续运行时:

拿到的已经不是默认配置。

这种问题在:

全局配置。

Singleton。

内存缓存。

Feature Flag。

静态变量。

里尤其常见。

最麻烦的是:

代码逻辑本身完全没错。

真正错的是:

测试之间偷偷共享了状态。


四、第三类污染:Mock没有恢复

假设测试A为了模拟支付失败:

mock(payment_service).return_error()

如果这个Mock在测试结束后没有Reset,

测试B再调用支付服务时:

它仍然得到失败结果。

于是你看到的是:

test_payment_success FAIL

Codex如果只分析当前测试,很可能会怀疑:

业务代码。

接口逻辑。

甚至测试断言。

但真实问题却发生在前一个测试里:

Mock生命周期管理失败。

所以看到“单独跑通过、一起跑失败”时,

Mock恢复一定要优先检查。


五、第四类污染:缓存、文件和环境变量

还有一些问题更隐蔽。

比如测试A写入Redis:

user:123 = old_value

结束以后没有清理。

测试B开始以后直接命中缓存。

它根本没有走自己预期的数据库路径。

或者测试A生成:

/tmp/result.json

测试B看到文件已经存在,于是走了另一条逻辑。

甚至测试A修改:

ENV_MODE=test_a

后面的测试继续继承这个环境变量。

所以测试污染并不只发生在数据库。

任何跨测试共享的资源都有可能造成问题:

数据库。

Redis。

全局变量。

Mock。

文件。

环境变量。

端口。

时间。

随机数。


六、为什么失败看起来会“随机”?

因为测试执行顺序不一定永远一样。

尤其当项目使用:

并行测试。

随机顺序。

分组执行。

不同CI Worker。

以后,同一个测试前面跑的是谁可能不断变化。

假设:

测试B只有在测试A之后才失败。

那执行顺序:

A → B

就会失败。

但:

B → A

可能完全正常。

于是你看到的表象就是:

测试偶发失败。

但它其实并不随机。

真正隐藏的条件是:

Execution Order——执行顺序

只不过这个条件没有被你显式看到。


七、ChatGPT、Codex最容易犯的错误:一直盯着失败测试改

比如:

test_update_inventory FAILED

Agent很容易开始:

分析inventory逻辑。

修改断言。

增加等待。

Retry。

放宽条件。

但如果这个测试单独运行一直稳定通过:

第一反应其实应该改变。

不要先问:

“为什么这个测试失败?”

而应该先问:

“它前面运行了什么?”

这两个问题完全不同。

前一个把注意力放在失败点。

后一个开始寻找:

污染源。

这通常才是正确方向。


八、真正有效的定位方法:改变测试执行顺序

遇到这种问题,第一步不要改代码。

先做:

Shuffle Test Order——随机化测试顺序

把整套测试随机执行多次。

如果失败测试不断变化:

测试隔离问题的概率就很高。

然后进一步缩小范围。

比如发现:

只有:

test_a
test_b

一起运行时:

test_b失败。

那就分别尝试:

test_b

以及:

test_a test_b

如果结果是:

test_b                 PASS
test_a test_b          FAIL

那基本已经非常明确:

test_a污染了test_b依赖的状态。

这种二分缩小范围的方法,比反复读失败测试高效很多。


九、第二步:建立“测试前后状态Diff”

如果怀疑数据库:

测试A执行前后分别记录:

表行数。

关键记录。

事务状态。

如果怀疑Redis:

记录相关Cache Key。

如果怀疑全局变量:

记录配置值。

如果怀疑Mock:

检查Mock是否恢复。

思路其实很简单:

Before Test A
↓
Run Test A
↓
After Test A
↓
State Diff

真正要找的是:

测试A结束以后,留下了什么本来不应该留下的东西。

这就是Evidence。

比直接修改测试靠谱得多。


十、第三步:让测试拥有真正独立的Fixture

一个稳定测试最好满足:

自己准备输入。

自己创建资源。

自己执行。

自己验证。

自己清理。

不要依赖:

“前面那个测试应该已经创建过用户。”

也不要假设:

“数据库现在应该是空的。”

例如不要写:

user_id = 1

如果这个ID依赖测试执行历史。

更可靠的是:

每个测试自己创建独立用户。

或者每个测试使用:

独立数据库事务。

独立Schema。

独立临时目录。

独立Cache Namespace。

测试环境越隔离:

执行顺序越不重要。


十一、时间和随机数也是隐藏污染源

有些测试没有共享数据库,也没有全局变量。

但仍然偶发失败。

这时候要检查:

时间和随机数。

例如:

now()

或者:

random()

如果测试依赖真实时间:

跨秒。

跨分钟。

时区。

夏令时。

都可能改变结果。

如果使用随机数据但没有固定Seed:

某些随机输入可能刚好触发边界条件。

这类问题虽然严格来说不完全是“测试之间污染”,

但表现非常相似:

单独跑正常。

整套执行偶发失败。

所以排查时也要确认:

时间是否可控。

Random Seed是否固定。


十二、并行测试会进一步放大测试污染

项目规模变大以后,测试经常会并行运行。

这时候原本很难出现的问题会突然暴露。

比如两个测试同时使用:

同一条用户记录。

同一个Redis Key。

同一个临时文件。

同一个端口。

同一个Mock Server。

于是原本串行时:

全部通过。

开启并行以后:

大量随机失败。

所以如果问题只出现在CI,而本地很难复现:

一定要比较:

CI是不是开启了Parallel Test。

Worker数量是多少。

资源是不是共享。

很多所谓“CI不稳定”,本质上其实是:

测试隔离不足,只是以前被串行执行掩盖了。


十三、怎么让Codex更快定位这类问题?

不要只把最后一个Error丢给它。

更有效的是给它这些信息:

单独运行:PASS
整套运行:FAIL
失败测试每次可能不同
CI开启并行

然后明确要求它优先检查:

共享状态。

Fixture生命周期。

数据库清理。

Mock恢复。

Cache Key。

临时文件。

执行顺序依赖。

这样Agent的排查方向会完全不同。

因为它知道:

现在找的不是普通业务Bug。

而是:

Test Isolation Failure


十四、给自己测一个指标:测试污染率

这篇我建议只看一个核心指标:

Test Pollution Rate——测试污染率

可以做一个很简单的测试:

将整套测试随机调整执行顺序100次。

统计其中有多少次因为:

共享状态。

顺序依赖。

环境残留。

导致本来单独通过的测试失败。

例如:

100轮随机执行里有7轮失败。

那么:

测试污染率 = 7%。

这个数字比:

“测试偶尔不稳定”

有意义得多。


十五、测试污染率高,应该先解决什么?

如果测试污染率比较高:

先看:

数据库有没有Reset。

Fixture是否真正独立。

Mock有没有Restore。

缓存是否清理。

临时文件是否隔离。

时间与随机数是否固定。

并行测试资源是否冲突。

最重要的一点是:

不要靠Retry掩盖问题。

因为重跑以后成功,只能证明:

当前顺序没有再次触发污染。

并不能证明问题消失了。


十六、怎么降低测试污染率?

最有效的方向其实就四个。

第一,每个测试自己准备数据

不要依赖其他测试留下的状态。

第二,每个测试结束后恢复环境

数据库Rollback。

Mock Restore。

Cache Clear。

临时资源清理。

第三,让共享资源命名隔离

比如:

独立Cache Prefix。

独立临时目录。

独立测试用户。

第四,把测试顺序随机化放进CI

不要一直用固定顺序。

随机顺序越早暴露问题,

越不容易等到项目变大以后再集中爆发。


十七、Plus和Pro怎么判断?

如果你的测试污染率还比较高:

单个测试经常通过。

整套运行却随机失败。

CI反复Retry。

Agent大量时间都在:

重新跑测试。

猜失败原因。

修改本来没有问题的测试。

那当前真正限制效率的不是ChatGPT、Codex容量。

而是:

测试体系本身不够稳定。

这种阶段Plus通常已经够用。

更值得先解决:

Fixture隔离。

共享状态。

Mock恢复。

缓存清理。

并发测试资源冲突。

否则增加更多AI容量:

只是让Agent更快地反复处理同一批随机失败。


如果你的测试污染率已经长期接近0:

测试执行顺序基本不影响结果。

并行执行也稳定。

失败能够稳定复现。

Agent可以根据可靠测试结果继续修改。

同时又长期存在:

大量复杂工程任务排队。

多个Agent持续处理项目。

AI容量真正开始成为瓶颈。

这时候Pro才更容易放大真实效率。

因为Agent依赖的是:

一个可信的测试反馈系统。


最后

单个测试都通过,整套测试却随机失败,

最容易让人产生一个错觉:

“这个测试不稳定。”

但很多时候真正不稳定的不是测试本身。

而是:

测试之间的边界。

一个测试留下数据库数据。

另一个留下Mock。

第三个修改全局配置。

第四个写入缓存。

单独执行时:

每一个都完全正确。

放在一起以后:

它们开始互相改变对方的运行环境。

所以遇到这种问题时,不要第一时间让ChatGPT、Codex去“修失败的测试”。

先问一句:

这个测试单独跑为什么能通过,它前面到底发生了什么?

很多随机失败的根因,就藏在这两个测试之间。

持续分享 Codex、大模型开发与 AI 编程实战内容。
长期深度使用各类代码大模型,也整理了
稳定的Plus/Pro会员订阅渠道,有需要可自取。

Logo

AtomGit AI 社区提供模型库、数据集、Agent、Token等资源

更多推荐