文章目录前言1 先别慌用「单跑分层法」把问题拆开2 顺藤摸瓜我抓到了一张「幽灵表」3 为什么失败「零散且每次不同」4 修复两个动作缺一个都白搭4.1 动作一只清「数据库里真实存在」的表4.2 动作二异常路径直接扔掉整个连接池5 这次最该认的错第一轮归因6 能带走的 4 条原则P.S. 推荐一个大神的教程给想要了解或者学习人工智能知识的读者这个教程里内容讲解通俗易懂且风趣幽默对我帮助很大。我想与大家分享这个宝藏教程请点击下方链接查看 传送门https://blog.csdn.net/qq_74013365前言跑一次全量测试21 个用例集体阵亡。把失败的那几个文件单独拎出来跑全绿。再全量跑一遍死的是另一批——上次是级联删除的断言炸了这次是另一个文件里的计数对不上。这场景熟不熟熟。说明咱俩都被连接池坑过算狱友。1 先别慌用「单跑分层法」把问题拆开出这种事第一反应通常是我代码是不是有并发 bug先按住这个念头。有个很便宜的判据把失败的文件单独跑全绿 → 说明业务逻辑没毛病问题出在用例之间的相互影响再把失败文件按不同顺序跑几遍失败集合变了 → 实锤是共享状态在传递。两步做完方向锁死不是被测代码的锅是测试之间的状态污染。就像排队单看每个人都是文明市民凑一起就有人突然咬人。问题不在人在队形。顺带说个同款案例测试缓存目录硬编码指到了生产目录A 用例写进去的缓存被 B 用例命中于是 B 那条「必须真实调用一次」的断言直接假死——典型的被隔壁老王带崩。所以看到「单跑绿、全量红」优先怀疑共享的可变外部状态缓存目录、状态账本、全局单例、连接池。被测代码先靠边站。2 顺藤摸瓜我抓到了一张「幽灵表」沿着状态污染往下查锁定了每个用例结束后的清理逻辑遍历所有表逐个 TRUNCATE 清空保证下一个用例拿到干净的空库。逻辑本身没毛病毛病出在表名单的来源fortableinreversed(Base.metadata.sorted_tables):conn.execute(text(fTRUNCATE TABLE {table.name}))这里用的是 ORM 元数据 Base.metadata。它是动态的只要任何一个用例在运行期 import 了一个新模型这张表就被加进元数据。但建表动作 create_all 只在测试会话开始时执行过一次。于是出现了一个诡异的中间态元数据里有这张表数据库里却没有。我管它叫「幽灵表」。名单上有它库里没它跟通讯录里那些永远打不通的号码一个德行。拿幽灵表去 TRUNCATEMySQL 反手一个 1146 Table doesn’t exist。异常一抛循环当场中断——排在幽灵表后面的真实表一个都没清。3 为什么失败「零散且每次不同」到这儿才解释了一半。循环中断只会导致数据没清干净但为什么炸的是级联删除这种八竿子打不着的断言关键在清理逻辑的最后一行conn.execute(text(SET FOREIGN_KEY_CHECKS0))fortablein...:conn.execute(text(fTRUNCATE TABLE {table.name}))conn.execute(text(SET FOREIGN_KEY_CHECKS1))# ← 循环中断这行永远轮不到SET FOREIGN_KEY_CHECKS 是会话级变量绑定在数据库连接上不是全局设置。看这条链幽灵表出现 → 循环在第 N 张表中断 → 末尾的 SET FOREIGN_KEY_CHECKS1 没执行 → 连接被归还进连接池时还揣着「外键校验关闭」的状态 → 下一个用例从池里复用这条连接 → 它活在一个外键约束不生效的世界里。外键校验一关级联删除不触发、引用完整性不校验依赖外键行为的测试开始莫名其妙地挂。至于具体哪个用例踩到这条脏连接取决于连接的复用顺序——跟开盲盒似的每次开出来还不一样。这就像你把车借出去还回来的时候油箱是空的、空调是开着的、座椅被调到了最低。连接池就是那家接车不检查的租车行。4 修复两个动作缺一个都白搭4.1 动作一只清「数据库里真实存在」的表从源头避免中断拿元数据和 information_schema 取交集幽灵表自然被过滤withtest_engine.begin()asconn:existing{row[0]forrowinconn.exec_driver_sql(SELECT table_name FROM information_schema.tables WHERE table_schema DATABASE())}conn.execute(text(SET FOREIGN_KEY_CHECKS0))fortableinreversed(Base.metadata.sorted_tables):iftable.nameinexisting:conn.execute(text(fTRUNCATE TABLE {table.name}))conn.execute(text(SET FOREIGN_KEY_CHECKS1))4.2 动作二异常路径直接扔掉整个连接池exceptException:test_engine.dispose()raise这条比第一条更重要。只要还存在任何一条能让 SET FOREIGN_KEY_CHECKS1 执行不到的路径就有泄漏的可能。与其逐个堵漏不如加一条兜底清理过程一报错整个连接池全部作废重建。宁可多花几十毫秒重建连接也绝不让一条「外键关着」的连接流出去——跟出门前检查煤气一个道理。对连接池要么把东西还干净要么把连接就地销毁别跟它讲道理——它听不懂。修复后验证那个此前反复翻车的级联删除测试文件单跑 20 个用例全绿再把它和其他几个文件串联跑两遍两遍都是 62 passed。5 这次最该认的错第一轮归因这次排查最该记下来的不是技术细节是上一轮的归因错了。第一轮排查时日志里有一批 1146 Table doesn’t exist。当时全量测试同时被另一个问题干扰——宿主机内存溢出数据库进程周期性被杀测试成片报连接错误。于是我把那批 1146 一并打包进了「环境噪声」还写下了一个错误结论表在跑的过程中消失了。事实完全相反表一直都在是表名单里混进了数据库里根本不存在的表。一个是「东西没了」一个是「名字错了」排查方向天差地别。把 1146 打包进环境噪声就像钥匙丢了怪天气——天气背不动这个锅。沉淀一条判据1146 / 1142对象不存在是结构层错误不是连接层错误。连接层错误是 2003 / 2013 / 1205 / 1213。把「对象不存在」打包进「环境问题」就会掩盖一个真实的测试隔离缺陷。更通用的一条噪声只是根因的遮盖物不是根因的替身。清掉噪声之后必须回头把之前的归因重跑一遍否则你会带着一个错误结论继续跑它迟早会在更贵的地方咬你一口。6 能带走的 4 条原则**「单跑绿、全量红」先怀疑共享可变状态。**顺序缓存目录 → 状态账本 → 全局单例 → 连接池。别一上来就怀疑被测代码。**连接池会继承连接的所有会话级状态。**SET FOREIGN_KEY_CHECKS、SET SESSION sql_mode、临时表、事务隔离级别——任何会话级设置都是借来的东西用完必须还还不了就把连接销毁。**清理逻辑的错误处理优先「销毁」而不是「继续」。**清理中途失败最危险的选择就是记个日志往下走。把连接池整个丢弃是便宜的脏连接污染出一次假绿测试是昂贵的。**只比对静态清单去操作数据库对象是脆的。**任何「按代码里声明的清单去操作实际存在的资源」的动作都应该先用一次 introspection 求交集。代码里声明的东西和现实之间存在时间差这个差值是 bug 的高发区。做自动化这几年最耗时间的从来不是写代码是摸清每个平台的脾气。它脾气不好你得忍着。如果你手上也有重复度很高的活儿——批量发布、数据搬运、有固定规则的机械操作——评论区说说你的场景我看看能不能给你自动化掉。P.S. 推荐一个大神的教程给想要了解或者学习人工智能知识的读者这个教程里内容讲解通俗易懂且风趣幽默对我帮助很大。我想与大家分享这个宝藏教程请点击下方链接查看传送门https://blog.csdn.net/qq_74013365