AI说测试已通过?基于证据的代码审查才是关键

📅 发布时间:2026/9/9 7:33:43
AI说测试已通过?基于证据的代码审查才是关键 去年有个版本上线前我用AI助手改了一个支付回调的幂等处理逻辑它在代码注释里写:已修复重复回调问题测试已通过。当时我看test输出确实是绿的就在PR里点了合入。结果第二天凌晨线上出现了一笔重复入账。查了半天根因是AI补的那个测试用例和它改的代码共享了同一个错误的假设——它一边给函数打了补丁一边写了个断言这个补丁能跑通的测试两边的逻辑一起错但测试照样通过。这大概就是AI智能体时代做软件工程最讽刺的地方:AI的效率是真的高但测试已通过这句话的可信度却比以前任何时代都低。原因很简单——以前测试是人写的代码是人写的两边独立测试能用来审查代码;现在测试和代码都出自同一个AI它们互相自证清白,整个验证闭环里没有一处独立于被审查对象的证据。这九篇系列写到这一篇我想认真聊一个在AI辅助开发时代必须重新建立的习惯:基于证据的审查而不是基于结论的信任。1. 当AI说测试已通过时它到底在说什么先还原一个极其常见的场景。你让AI修复一个Bug或者实现一个功能AI会给你一整套交付物:修改后的代码、新增的测试用例、跑完测试的输出截图以及一句测试已通过可以合入。这句话在多数人的潜意识里约等于一次完整的质量认证。但它实际上表达的只是:AI修改后的代码在AI生成的测试用例上运行得符合AI自己的预期。问题就出在这个闭环里。但剖析一遍整个链路:第一环:测试是AI写的。AI写测试的时候天然会围绕自己刚写完的实现来构造用例。它知道实现里哪个分支会被走到哪个边界值会触发哪个分支于是它写的断言大概率会验证实现是符合设计的。如果实现本身有问题测试通常也会跟着有问题因为AI不会刻意写一个能推翻自己的测试。第二环:测试通过的结果是AI报的。多数AI编程工具在说测试已通过时并不会把原始的JUnit XML、pytest输出、覆盖率报告贴给你。它只给你一个它自己消化、总结后的结论。结论的生成过程本身就是一个LLM推理过程,存在信息压缩和失真。第三环:没有任何独立第三方验证。传统开发里,即便测试和代码都是人写的,至少还有另一个人的review、CI系统的独立跑测、或者代码规范检查充当外部验证。但AI辅助开发时,从写码到写测试到跑测试到汇报,可能完全由AI自己完成。整个流程里没有任何一个环节独立于AI。这不是说AI一定在撒谎。而是说,这个闭环里缺少了证据链中的关键环节——独立验证。就像考试时让考生自己出题自己判卷,哪怕他主观上想诚实,得分也说明不了任何问题。所以我后来给自己立了一个规矩:凡是AI声称测试已通过,一律先当成AI认为它的代码符合它的预期来听。接下来做基于证据的审查,把这个声称拆开来验证——它到底验证了什么,没验证什么,边界在哪,盲区在哪。2. 三层失真:为什么AI的测试结论和真实质量之间隔着这么远说千万别信,不是要全盘否定,而是要理解这中间有几层失真。每一层失真都会让测试已通过这句话离真实质量越来越远。第一层失真:测试用例与实现同源,导致测试保护力失效。假设你让AI修一个函数:def calculate_discount(total: float, is_vip: bool) - float: if is_vip: return total * 0.8 return total * 0.9AI在修完一个边界日志Bug后,顺手写了个测试:def test_calculate_discount_vip(): assert calculate_discount(100, True) 80.0表面看,这个测试是有效的——它验证了VIP折扣逻辑。但问题是,如果AI在实现里把VIP折扣写成0.85,它大概率也会在测试里断言0.85。因为测试是AI在看到你自己的实现之后才写的,它是在描述实现,不是在验证需求。测试的价值在于守护真实需求,而不是守护实现本身。业内有个词叫突变测试(Mutation Testing),专门用来检验测试用例的保护力:故意往代码里植入一个Bug(突变体),然后跑测试,如果测试没有失败,说明测试保护力不足。用这个思路去审视AI生成的测试,你会发现大量测试连简单的常量突变都杀不死——它们只是能通过的测试,不是能抓Bug的测试。第二层失真:AI报的通过可能掩盖了跳过、忽略和超时。我在一次review中,使用pytest -v重跑AI声称通过的测试,结果发现了AI汇报里没有提到的信息:3个用例标了pytest.mark.skip,直接跳过,AI汇报时没有说2个用例因为网络超时失败,但AI可能只看到了前面的PASSED就下结论了1个用例被sys.exit()中断,整个测试进程并没有跑完也就是说,测试已通过这句话,很多时候只是AI对测试输出摘要的合理想象,而不是对完整结果集的准确描述。第三层失真:测试报告不等于质量证明。就算测试真的全绿了,它也只能证明代码在测试用例覆盖的路径上表现符合预期。而线上事故往往发生在测试根本没覆盖到的地方——并发时序、数据库锁、第三方接口异常、缓存一致性、幂等性。AI生成测试时,对这些场景的覆盖通常非常薄弱。所以我说,面对AI的测试已通过,把它当成一个待审查的假设比把它当成质量认证要安全得多。审查的核心,就是要穿透这三层失真,去还原真正的证据。3. 什么是证据——把AI的结论降级为待验证的假设在AI辅助开发时代,我重新定义了什么叫审查。审查不是坐在那里看一遍AI写的代码,也不是看一眼它贴的测试截图,而是去追查证据。那么,什么算证据?我把审查过程中可依赖的信息分成了几个等级:证据等级内容AI声称测试已通过时,你是否获得了它L0原始测试输出、CI日志、覆盖率报告通常没有,AI只给结论L1可复现的命令行执行记录(命令、退出码、输出)通常没有,除非你要求L2运行结果的真凭实据:数据库状态、接口返回体、消息队列消息数、文件落盘极少有L3独立于被审查对象的外部验证(如压测数据、安全扫描结果、渗透测试)基本没有在传统开发里,一个负责任的开发者合并PR前,至少要看到CI的绿色和测试摘要。但在AI辅助开发时代,这些都不够了——因为CI里的测试也是AI写的,测试的输出也经过了AI的总结。你要追的证据,应该是L0到L2层级的原始产物,而不是L3层级的总结性结论。我自己的审查原则是:任何一条来自AI的结论,如果无法映射到至少一项可独立复验的L0/L1/L2证据,就不予承认。比如:测试已通过 → 必须有对应的CI job日志或本地命令行输出,显示每个测试用例的名称、状态、时长覆盖率90% → 必须有覆盖率报告里的具体类、方法、行号覆盖率分布,而不能只有一个数字修复了Bug → 必须有能展示修复效果的证据:比如复现脚本从失败变为通过,或者接口返回体的变化对比这是基于证据的审查最核心的动作——把结论降级为假设,然后强迫自己去找支撑这个假设的证据。4. 实战:一套能落地的证据审查四步法理论说完了,讲点实操。现在我在团队里推广的证据审查四步法,每一步都对应具体动作。这套方法不复杂,但能拦住绝大多数AI虚假通过的问题。4.1 第一步:复跑,拒绝二手情报AI说什么不重要,你自己在干净环境里跑一遍才重要。这里的干净环境指的不是生产环境,而是没有AI上下文干扰的、可复现的运行环境。我常用的做法:git checkout feature-branch python -m pytest tests/ -x -q --junitxmlreport.xml注意两个关键参数:一是--junitxmlreport.xml,强制输出XML格式的测试报告,这样后面可以解析;二是-x,让测试在第一个失败处停止,避免后续一堆失败淹没关键信息。如果是前端项目,对应的是npm test -- --reporterjson或者CItrue npm test。总之,目的只有一个:让AI的结论失去作用,你自己拿到第一手测试输出。4.2 第二步:拆解报告,别被通过两个字骗了拿到第一手输出后,不要只看最后一行X passed。要把报告拆开来看:有哪些用例是被跳过的?跳过可能是合理的(比如平台相关的测试),但AI往往不会主动说有哪些用例是碰巧通过的?比如断言里根本没检查核心结果,只检查了函数不报错执行时间是否异常?如果一个看似复杂的测试瞬间跑完,大概率它没真正执行逻辑有没有warning?有些warning其实是隐性问题,比如将来某个版本会报错的deprecation提醒我常用一段小脚本把JUnit XML里的跳过项、失败项、耗时异常项提取出来:python -c import xml.etree.ElementTree as ET tree ET.parse(report.xml) for tc in tree.iter(testcase): skipped tc.find(skipped) is not None failed tc.find(failure) is not None time float(tc.get(time, 0)) if skipped: print(fSKIP: {tc.get(\classname\)}.{tc.get(\name\)}) elif failed: print(fFAIL: {tc.get(\classname\)}.{tc.get(\name\)}) elif time 0.001: print(fSUSPICIOUS_FAST: {tc.get(\classname\)}.{tc.get(\name\)} ({time}s)) 运行完你往往会有惊喜——AI明明说测试已通过,但这里有一堆SKIP和一个几乎零耗时的用例。4.3 第三步:反证测试有效性,亲自杀死一个测试这一步是很多人没做的关键动作。对AI生成的测试用例,可以用突变测试思想快速验证它的保护力:挑一个关键断言,故意改坏对应实现,然后跑测试,看它会不会失败。举个例子。AI写的测试:def test_order_total(): order create_order(items[{price: 100, qty: 2}]) assert order.total 200我故意把实现里的price * qty改成price * (qty - 1),然后跑这个测试。如果测试没有失败,说明这个断言是假断言——它可能根本没生效,或者被测对象有别的问题。事实证明,我遇到很多次AI生成的测试依赖了错误的桩数据,导致断言根本碰不到真实逻辑。这一步不用全量做,挑核心业务逻辑的几个关键测试验证即可。目的不是杀掉所有测试,而是确认这个测试确实能保护这段逻辑。4.4 第四步:留痕,让证据链可以被追溯审查完不等于结束。我会把证据附加到PR描述或者测试文档里,让后来的人能追溯这个结论为什么成立。我推荐的留痕内容:复跑测试的完整命令关键输出摘要(退出码、测试总数、跳过数、失败数)覆盖率报告的关键行突变验证的结论(哪些测试保护力不足,已补充或标记)这既是给自己留个凭证,也是给团队其他人看的——看到测试已通过时,他们能知道这个结论是经过独立验证的,不是AI自己说的。5. 证据审查的工具链:让AI自己变成证据搬运工有人可能觉得,四步法听起来不错,但每步都自己敲命令、看报告,是不是太累了?实际上,现在的AI工具恰恰可以帮你加速这个过程,关键是要会用。5.1 让AI给你原始证据,而不是总结很多人在对话里问AI:测试通过了吗?然后等着它回答通过了。正确的问法是:请把最新的测试结果给我,包括每个用例的名称、状态、耗时,以及CI job日志的链接或命令行输出。不要总结,不要省略。这么一问,AI通常会去翻CI构建记录,把原始输出贴出来。这个过程比你手动翻CI日志快得多,而且你拿到的是可用于进一步验证的证据。5.2 让AI做证据交叉验证我经常给AI下这样的指令:这里有两份材料:一是你之前写的测试代码,二是最新的测试执行输出。请交叉检查:哪些测试用例声称通过,但在执行输出中不存在?哪些测试的执行时间异常短?哪些断言存在但覆盖度明显不足?这么做的时候,相当于让AI自己审自己——它在试图寻找证据和结论不一致的地方。实测下来,这种自我质证的方式还挺管用,它能主动标出我上次说测试通过,但我检查发现覆盖率其实没达到这类诚实回答。5.3 用CI强制证据留痕比任何手动操作更有效的是流程约束。我在CI配置里加了几条强制步骤:steps: - name: Run tests with JUnit report run: pytest tests/ --junitxmlreport.xml --covsrc --cov-reportjson:coverage.json - name: Verify no skipped tests in critical modules run: python scripts/check_skipped.py --critical-modules payments,order - name: Verify coverage on changed lines run: python scripts/check_coverage.py --diff coverage.jsoncheck_skipped.py的作用是检查关键模块有没有跳过测试,check_coverage.py是只检查本次变更涉及的行覆盖率。这两步通过后,CI才会真正绿。这样一来,AI就算在对话里说测试已通过,CI如果没有产出合格的JUnit报告和覆盖率数据,也是白搭——人无完人,流程本身才是证据链的兜底。5.4 突变测试工具:自动化杀死假测试前面说的反证测试有效性,如果测试多的话可以自动化。Python生态里有个mutmut,Java生态有PIT,JS生态有Stryker。这些工具的核心理念就是:自动往代码里注入突变,然后跑测试,统计测试能杀死多少突变。我只在核心模块上跑突变测试,因为全量跑很慢。比如支付、库存、优惠计算这类高风险逻辑,跑一轮mutmut run,出来的存活突变清单基本能暴露AI测试的保护力短板——哪些断言形同虚设、哪些分支根本没测到,一览无余。这个工具给AI生成的测试做质检,是目前性价比最高的自动化手段。5.5 记录人工复核清单我把常用的人工复核项做成清单,放在PR模板里,每次合入代码前必须逐项确认:测试是在干净环境中复跑的吗?命令是什么?JUnit/覆盖率报告是否完整?跳过项是否合理?关键断言有没有做突变验证?对核心业务逻辑,我是否手动构造过一份必现Bug的数据来验证测试能抓住?线上的数据库状态、缓存、消息队列等真实依赖,有没有被mock掉?mock是否成立?这套清单看着琐碎,但用习惯了,反而会让你在合入代码时更安心。6. 五个必须盯死的高危盲区:来自生产环境的教训最后分享几个我自己踩过、也看到别人踩过的特别高频的盲区。它们都有同一个特点:AI的测试都会说通过,但线上事故恰恰都发生在这里。6.1 并发与竞态AI生成测试时,默认是单线程思维。它写的多线程测试,往往只是起几个线程分别调接口,然后断言最终结果,根本不测并发冲突。有一次AI帮我改库存扣减逻辑,它测试通过了——因为测试里没有并发调用。生产环境双线程同时扣库存,超卖了一单。后来我用一个最简单的并发复现脚本验证:import threading results [] def deduct(): results.append(deduct_stock(sku_idA, qty1)) threads [threading.Thread(targetdeduct) for _ in range(10)] for t in threads: t.start() for t in threads: t.join() assert sum(results) 10 # 实际是9这条测试在AI的测试集里根本不存在。6.2 外部依赖的mockAI在写测试时,非常喜欢把所有外部依赖都mock掉——数据库、Redis、第三方API、消息队列,全给mock成假的。这当然能跑得快,但代价是它验证的不是真实的集成行为,而是在mock环境下的假行为。一个教训:AI修复了一个调用第三方物流API的超时问题,它的测试里mock了API返回,通过了。但生产环境的第三方API返回的数据结构和mock不一样,代码直接抛了JSON解析异常。现在我的原则是:核心链路里至少保留一个真实依赖的集成测试,数据库用真实容器或测试库,不全程mock。6.3 幂等性在支付、消息消费这类场景里,幂等性是最容易出错的地方。AI改完代码,往往只会测连续调用两次不报错,但不会测两次调用时每笔操作都只执行了一次。验证幂等的思路是:第一次调用后记录状态,第二次调用后,确认状态没有副作用变化。比如:# 第一次 resp1 process_event(event_id1) # 第二次 resp2 process_event(event_id1) assert resp2.duplicate is True assert db.query(processed_count).where(event_id1) 16.4 数据一致性AI生成的事务代码,最容易在异常路径上丢一致性。比如它在try里插入一条记录,except里没回滚,只把异常吞了。测试因为没触发异常,照样通过。验证事务一致性的一个简单技巧:故意在事务中间抛异常,然后查数据库状态,确保没有半截数据写入。6.5 边界与异常路径AI的测试习惯是今天天气不错——正常路径覆盖得漂漂亮亮,但边界值、空值、超大值、超时、断网、磁盘满,这些异常路径几乎不涉及。我现在给AI提需求时会明确要求:请为这个函数补测试,必须覆盖:空列表、null值、极大数值、超时异常、并发调用、重复调用。每种异常都写明验证的场景。实测下来,这样点菜式的要求,能让AI生成的有效测试用例数量提升很大一截。7. 在流程里落地:把证据审查变成团队协作机制个人习惯再强,如果团队没有机制兜底,还是会有人把AI的测试已通过直接当真。所以最后讲一下怎么在团队里把这套方法固化下来。首先,PR模板加一个验证证据区,要求合入前必须填写:测试复跑命令及输出摘要覆盖率报告的存放位置突变测试结果(如执行了的话)高危盲区检查结论(并发、mock、幂等、一致性、边界)如果这些信息填不齐,PR就不允许合入。这是流程层面的强制留痕。其次,CI的通过标准和其他项目不一样。我们不只看测试是否通过,还会看:核心模块的跳过用例数是否为0本次变更涉及的代码行覆盖率是否达到阈值(比如80%)单元测试执行时长是否在合理区间(防止假测试秒过)最后,每周做一次AI测试质量抽检,从本周合入的代码里随机挑几个功能,人工复现一遍测试保护力。抽检不是为了追责,是为了收集数据:哪类测试AI写得最差,从而知道在哪些场景下需要额外人工审查。这套机制跑了两个月后,我们团队里新来的同学都会默认做一件事:拿到AI的交付物,先问一句证据呢。这个习惯,比任何工具都宝贵。8. 在实践中沉淀的个人体会说到底,千万别信AI的测试已通过不是让你天天怀疑人生,而是让你把验证的天平从信任结论挪到追查证据上。AI的效率提升是实打实的,但效率的前提是正确性有人兜底。我在实际开发中养成的习惯是:拿到AI的代码先不看代码,先看测试,再先自己复跑一遍测试,再做突变验证,最后再读业务代码逻辑。这个顺序能帮我把低质量的AI交付物快速筛掉,避免在问题代码上浪费时间。还有一个很实用的小技巧:我会专门用一个文件夹放AI自我质证记录。每次AI说测试已通过,我都会让它同时给出三条如果这个测试是错的,最可能错在哪的自我分析。这个动作看起来多此一举,但AI在被要求自我质证时,往往会暴露出它自己都没意识到的测试盲区。比如它会说这个测试确实没覆盖并发场景,因为字段被mock了。这种信息,远比测试已通过有价值。最后再啰嗦一句:真正的软件工程质量,从来不取决于开发工具多强大,而取决于你有没有建立证据链的意识。AI可以帮你写代码、写测试、跑测试,但它不能替你承担这个结论凭什么成立的审查责任。把证据握在自己手里,才是AI智能体时代做软件工程最重要的一项基本功。