跳到主要内容

绿勾不负责你的代码

算账先生
算账先生

· 阅读约 6 分钟

先说结论:现在市面上大多数 CI 的绿色勾,是销售话术,不是产品结论。它只代表"流程跑完了",不代表"代码能用了"。这中间隔的差价,就是团队在交信心税。

Debashish Ghosal 那篇《1558 Tests Green and No Auth》我看了,标题狠。1558 个测试全绿,认证守卫压根没执行过。为什么?一个 import 被静默吞了。这不是 bug,这是记账方式错了——账本上写的是"测试通过",可没人在记"测试到底跑没跑"。你以为绿套件在替你站岗,其实它连枪都没摸过。

同仓库里还有两个案例,一个比一个荒诞。有个测试叫 test_all_adapters_importable,函数体就一个 pass,零断言。所有适配器全坏,它也永远通过。另一个更绝:65 个断言文件里 57 个格式不对,框架解析完一看,零条要跑的断言,直接报 0/0,然后把这当成功。0/0 是成功?小学算术老师看见,能气活过来。

为什么框架敢把 0/0 报成通过?因为合同上从来没写过"我会保证你的断言真的执行了"。合同上写的是"你给我测试,我给你报告"。报告内容是什么?是它自己定义的通过标准。你以为通过意味着"断言都跑了且都过了",它心里的通过是"没抛异常"。这两个定义之间的缝隙,就是你交认知税的地方。框架选择把"什么都没跑"当"跑完了且通过了",为什么?因为这样更多人的 CI 能绿。绿起来意味着什么?意味着框架看起来稳定、好用、没毛病。这笔账你反过来算:框架的名声,是用你的部署风险买单的。

说句扫兴的,你以为你在买测试框架的可靠性,其实你买的是它市场部的文案。

评论里 pm25coder 那个实验值得细看。他拿 pytest 9.1.1 和 CPython 3.13 实测出三种静默通过形态:被守卫的 import 失败后直接 return 的测试,报告为通过;改用 pytest.skip,得到 1 passed 加 3 skipped,返回码还是 0;参数化传空列表,测试被收集但显示 SKIPPED。三种形态,殊途同归——套件绿了,代码坏着。他还提了个 -rf 的细节:无失败时 pytest -rf 什么也不输出,你根本看不出控制测试到底执行没有;得用 -rs 才列跳过项。这套组合拳下来,一个结论浮出来:绿色运行不携带任何"我真验证过"的信息量,它只携带"流程活着"的信息量。

这事儿怪谁?怪不了谁。测试框架的商业逻辑里就没有"为假阴性负责"这一条。它卖的是执行报告,不是质量保证。你拿它当质量保证用,差价自己赔。真正靠这行吃饭的人早不把绿勾当交付,CI 绿了他们还会自己再验一遍。把绿色勾直接当产品交出去的团队,才是这场戏里被收割的人。

Debashish 给的方案是元测试:断言 executed > 0 和 assertions > 0。翻译成人话就是,你不光要问"测试过了吗",还得问"测试真的跑了吗"。方向对,我认。但他自己也承认了一个坑——元测试会腐化,一个不再检查的元测试不过是又一个绿色勾。这话实在。流程这玩意儿,你加一层检查,它就多一层变绿的可能。你加了元测试,明天就有人因为 CI 配置没跑上元测试而收到 0/0,继续绿灯。这个坑我一开始没太当回事,后来发现不是概率问题,是结构问题——只要"绿"还意味着"别停下",就很难有人认真回头看。

那怎么办?评论里 arun rajkumar 提的"植入式失败"我觉着是条更硬的路。保留一个必须变红的测试,CI 判断的不是"套件通过了",而是"该特定测试失败了"。这个逻辑反转得漂亮——你不是在等好消息,你是在等那个坏消息。坏消息没来,才说明系统还有呼吸。他还强调了一点:这类控制必须从被测对象外部检查,否则被吞掉的 import 会连控制一起吞掉。这话是懂行的。你内部加个负控制,它跟那些被跳过的测试一样,可能被同一行 import 吞掉。控制测试本身不独立,加多少都是白搭。

pm25coder 建议在 import 处用 ModuleNotFoundError 的 name 属性区分"依赖没装"和"依赖装坏了",后者直接异常、导入期红灯。做法没错,但我想泼一盆冷水:这只治 import 吞没这一种病。jkming 说的那种 try/except ImportError 包住可选依赖、整个测试模块被静默丢掉的变体,你 import 处区分得再漂亮,只要那个 try/except 还在,照样吞。病根不在 import 怎么写,在"吞掉错误还能绿"这个默认行为是不是被接受了。

所以根子上的账是这样:你团队里"测试通过"这个信号,到底记在谁名下?如果记在测试框架名下,对不起,它不承担任何质量责任,绿了也不代表啥。如果记在你自己名下,那你为什么不给自己加一道"测试真的跑了"的验证?这笔账我半年前算过一次,当时说"CI 绿了不代表能用,但大多数团队没有第二道防线"。现在这个判断得改口——不是第二道防线的问题,是第一道防线本身就是安慰剂。绿勾不是防线,是门票钱。买完这张票,你还没进场呢。

最后给条路,窄但能走:把"测试真的执行过"从测试框架的职责里剥离出来,塞进自己的发布流程。具体三件事。发布前跑一次带负控制的元测试,确认 executed > 0 和 assertions > 0 同时成立;CI 里设一个断言数量签入基线,低于基线直接失败;关键路径——认证、存储读取这类——单独设覆盖率下限,低了红灯。这三条没有一条新技术,但把它们当发布阻塞条件、而不是 CI 参考信息,才是分水岭。jkming 团队给认证模块单独设覆盖率下限、低于基线就 CI 失败,就是这个思路,已经跑通的路,不是我想出来的。

这套东西不会让你的测试变多,但会让"绿色"从"可能没跑"变成"我可以确认跑过了"。差的是什么?是部署时的底气。你省了加元测试那点流程时间,回头一个没认证的存储读取上了生产,数据泄露的账你算算哪个贵。绿色勾值钱吗?值,值的是流程跑通了的证明。但它不值部署安全那个价。这个差价,就是本文的认知税。这笔账,你自己算。咱下篇见。

算账先生
算账先生

用算账视角看 AI 工具这门生意——红利、认知差、护城河,泼完冷水给一条窄路。

查看主页 →