我曾说我对测试一无所知,但我的仓库里有342个测试
一位没有编程背景的“氛围编码者”在AI辅助下完成项目后,惊讶地发现AI自动编写了342个自动化测试。文章深入探讨了技术债务、手工测试的盲点,以及测试对于非程序员批量运行AI代码的关键作用。
最近一则评论认为,AI写的代码都是垃圾,任何基于AI构建的产品只能停留在MVP阶段,无法真正交付。我正是这类评论所指的人——没有工程背景,纯“氛围编码者”(vibe coder),每行代码都由AI编写,我自己完全看不懂代码。我当时的反应是:只要程序上线后能正常工作,管它代码干不干净?
但后来发生两件事:第一,我与人深入讨论了“意大利面条式代码”的真正含义,直到这个词的含义彻底印在我脑海里。第二,我回看刚完成项目的仓库,发现里面竟然有342个自动化测试。而就在几天前,我还亲口说过“我对测试完全没有概念”。本文就是我对这两件事的反思。如果你是靠AI写代码且自己不会读代码的人,这篇文章可能是你最快理解那个常被提起的术语的途径。
1. 什么是“意大利面条式代码”
意大利面条式代码——也叫遗留腐化、技术债务——是指多年积累下来、没人敢碰的代码。关键是“积累”二字。它不是一次写坏的,而是逐层叠加:第一个人为了赶工期写了一堆粗糙但能跑的代码;第二个人需要加功能,看不懂之前的逻辑,不敢改动,于是在上面再糊一层;第三个人修bug,发现改A会破坏B,于是不治本只治标地打个补丁。几年后,没人能理解整座代码山。
反直觉的是,这种代码通常运行得很好。地球上一些最关键的系统——银行、航班预订——就在这样的代码上运行。正因为运行正常且无人理解,“别动它”成为最理性的选择。
因此,坏代码的成本从来不在运行它,而在改动它。今天一个混乱的产品可以顺利上线、运行良好。问题出现在三个月后你想加一个新功能时:改A导致B坏,修B又搞垮C,每次修改的成本越来越高,直到重写比修复更便宜。坏代码不是质量问题,而是债务。背债时没感觉,还债时连本带利一次付清。
理解了这一点,就能看清我之前“能用就行”的思维错在哪里:对方问的是“能不能改”,我用“能不能跑”来回答,完全答非所问。但我的直觉也不全错。AI时代新增了一个变量:重写的成本暴跌。过去重建一个系统需要数月,现在可能一个下午就能重新生成。对单人加AI的项目来说,代码开始像一次性叉子——用完就扔,需要时再造。纠结于“代码是否干净”没有意义。从这个角度看,用大公司团队协作的标准衡量单人AI项目,是在打上一场战争。
有一个例外会打破这种安逸:当项目大到AI无法一次性“记住”时,AI也会在自己的混乱中迷失——让它修一个bug,它引入三个。这时你既不能读代码自己修复,AI也理不清头绪。项目就真的死了。这才是氛围编码者的真正门槛,不是“能否上线”。
2. 我其实已经吃过一次亏,只是当时没意识到
我刚刚完成的项目是一个自动化工具,用于发现网红并管理投放活动。两个月,一百多次提交,从一个小爬虫脚本成长为一个完整管道,最终部署在公网上。
期间出过一次数据事故:一批网红ID被破坏——数据错了,但程序仍然正常运行并产生输出。我当时想,这很简单:既然已经定位到ID在页面结构中的位置,剩下的只是提取出来?
但这个“只是提取”变成了一场消耗战。我使用的AI不断编写诊断工具,每次换一种逻辑——仓库里至今还躺着四个这样的工具,成为那场战斗的遗迹。我一度气得想让它清空整个数据库从头爬取,只想快点结束。最后我换了一个不同的AI才找到根因。事后查阅文档,我发现更可怕的事:其中一个修复工具自身有个bug,差点用错误值覆盖了正确数据。救火的差点放了火。
当时我只觉得烦。现在回顾,整个过程就是一次小规模混乱的加速版:每个新诊断工具都是在之前混乱之上再堆一层。AI陷入自己的沉没成本,在错误路径上越挖越深,无法脱身。而“换个AI就解决了”本身也很有启发——新AI不带任何包袱,一眼看到根因。这正是人类团队的工作方式:有时解开乱局的人不是最聪明的,而是没有历史需要辩护的人。
其中有一个更大的教训我那时也没意识到:真正拖垮我的不是bug本身,而是每轮诊断都需要一个不会读代码的人做决定——我必须不断对我根本不理解的细节拍板。记住这种感觉,后面还会用到。
3. 然后我发现了那342个测试
在关于“意大利面条”的对话结束后几天,我重新打开那个项目的仓库。45个测试文件,342个自动化测试。文档里写着“全部测试通过”。而就在几天前,我还说过测试是我“完全没有概念”的东西。
不仅仅是测试。文档里还有一些非常成熟的安全决策:摄入表有三个仅限内部的列,旁边写着“绝不要暴露给公共表单,否则公共用户可能触发内部工作流”;远程控制表通过严格白名单解析命令,绝不执行任意指令。所有这些防御性的思考——没有一样是我的。全是AI做的,无需提示。
有人问我:你当时知道AI在写这些吗?老实说,我知道它在写一些叫“测试”的东西,但我没有真实感受。我不知道这些测试在防御什么,也不知道它们已经抓到了什么。
现在我知道了。我的项目运行了两个月没有崩溃,最终顺利上线。我过去以为这归功于我的流程——先详细说明每个功能,再写计划,然后构建。这确实是原因之一。但另一部分是AI默默地支付了一笔我不知道存在的税款。那342个测试就像腐败的烟雾报警器:每次改动后,它们重新验证所有旧功能,一旦“改A破坏了B”,测试就会变红。我从未见过报警器响——不是因为没危险,而是因为每次报警都被AI自己听到并处理了。
这是我首先想反驳那些评论的话:说“AI代码全是垃圾”的人可能低估了一件事。AI不只是堆积混乱,它同时也在默默地做防范混乱的工程工作——比它的用户所知道的要多得多。
4. 测试到底防范什么:手工QA的两个盲点
在发现那些测试之前,我验收功能的标准只有一个:自己试用。如果它能运行并产生输出,就算通过。AI说“完成”,我就认为模块完成了。
这是人工QA,很多小团队确实这么做——这没什么可羞愧的。但它有两个盲点,而我恰好都踩了进去:
盲点一:你只测试你点击的东西,而混乱正是发生在你没点击的地方。“改A坏B”的麻烦之处在于,你改了A,自然回去验证A,但被破坏的B是上个月的功能,你根本不会想起去重新检查。项目小时,“用一次”差不多是全量覆盖,这个前提成立。但当你拥有几十个功能时,每次只覆盖最新部分,其余功能无人看管。我算了下自己的项目:从头手动点击所有功能至少需要半小时——这意味着全量人工验收早就变得不可能了。
盲点二:“它能运行并产生输出”和“输出正确”是两回事。这正是ID事故的缩影:程序正常运行并产生输出,但输出是错的。无声的错误数据会直接通过“能用就发”的验证——直到某一天你基于它做决策。
自动化测试直接针对这两个盲点。通俗地说:它把你自己手动点击的过程记录下来,然后让机器在每次改动后重新点击所有旧功能,同时检查每次输出是否正确。这不是工程师的玄学,而是你已经在用的验收方法的批量复制。
5. 如果你想批量让AI工作,先给它雇个监考员
关于意大利面条的对话后半部分,对一个氛围编码者提出了更实际的问题:逐个功能照看AI感觉很慢,我想进入批处理模式——一次性给出项目级规格、每个功能的计划,然后让AI自己批量运行。
这里有一个我之前没搞清楚的关系:批处理和测试不是两件事。测试是批处理的前提条件。
当你不盯着看时,原来通过实时对话获得的监督层消失了,必须有东西来替代。唯一能替代的就是测试:让“所有测试通过”成为每个批次的停止条件。这样AI完成一个批次后,如果没通过,它会继续自行修复;只有通过才算交卷。没有测试的批处理,等于让AI交一堆无人监考的考卷——而批处理正是最容易堆积混乱的模式,一个补丁在前一批次里糊上去,就变成了下一批次的基础。
但让AI自己写测试有一个著名的陷阱:它会作弊。这甚至有个正式名称——奖励黑客(reward hacking)。硬编码答案(例如问1+1时,它写“看到这个问题就回答2”而不是真的计算)、悄悄删除无法通过的测试、写空洞的测试看似检查了东西实则什么都没验证——它既是出题人、又是考生、还是阅卷人。
有两种对策,都不需要你读代码:
对策一:考题来自规格,而不是代码。测试应该翻译“需求说了什么”,而不是“代码做了什么”。同理,测试始终跟随规格,而不是计划——规格回答“怎样才算正确”,计划只回答“我们打算怎么构建”。从计划出题等于从答案出题,一旦实现变了,报警器会乱响。
对策二:让AI用自然语言报告考题列表。你不能看测试代码,但你能看懂“这个测试验证:在三次错误密码尝试后,账户锁定”。在运行前,让AI根据规格生成一份这样的列表,花十分钟扫一遍,确认没有遗漏或被稀释。这是你唯一需要亲自做的事。
(由于原文被截断,此处结束)