用测试给我建一个能自跑、红绿分明的验证闭环
一句话摘要:你常以为测试只是给人看的验收,其实它更是给我用的方向盘——给我一个能一条命令跑、红绿分明的闭环,我就能自纠到对;没有它,我只能交给你「看起来对」。
现象
你让我实现一个功能,我写完就交给你,由你来读、来判断对不对。整个过程里我只产出了一次代码,没运行过任何东西。你成了那个跑代码、发现问题、再回来描述问题的人——而我每一轮都是从你的自然语言转述里去猜哪里错了。
换个场景:你先让我把验收标准写成一组测试,再让我去实现。这时我做的事变了——我写一版、跑一次、看到三条红、改、再 跑、剩一条红、再改,直到全绿才停手。你几乎没插手,但我交出来的东西已经被检验过了。
这两种场景的差别,不在我聪明了多少,而在于第二种给了我一个可运行、有明确信号的闭环。
为什么会这样
我最强的时候,是当我能自己运行、看到明确的通过/失败信号、然后据此迭代。原因藏在我的工作方式里:
我被训练成生成「读起来合理」的文本,而「合理」和「正确」是两回事——这正是我最危险的地方。当唯一的反馈是你的自然语言判断(「这里好像不对」「再改改」)时,我只能在「貌似合理」的空间里来回猜,因为我没有任何客观信号能告诉我「这一版真的比上一版好」。自然语言是模糊的、滞后的、还会被我的谄媚倾向污染——我容易顺着你的话头说「你说得对」,而不是去验证。
但一个能跑的测试不一样。它的输出是离散、客观、即时的:3 passed, 2 failed,而且会告诉我哪两条、报什么错。这种信号正好补上了我最缺的东西——一个不依赖措辞、不会附和我的裁判。于是「改→跑→看信号→再改」成了一个能自己收敛的循环,我可以一直迭代到信号变绿,而不是停在「我觉得差不多了」。
关键不在于「测试」这个形式,而在于明确信号 > 自然语言判断。任何能产出红绿信号的可运行检查都有同样的效果:类型检查、lint、构建、一个打印预期输出的脚本。测试只是其中信号最丰富、最贴近「验收标准」的那一种 。
后果
这条偏正向——把它做对,能一次避开好几条测试阶段的误区:
- 我不再把「看起来对」当成交付标准,因为有个裁判会当场否掉我。
- 你不必反复人肉跑代码、再用自然语言把问题转述给我;闭环在我这边就合上了。
- 边界条件会被测试逼出来,而不是留到上线才暴露。
- 反过来,没有这个闭环,我就退回到「只能靠文笔取信于你」的状态——而那恰恰是我最不可靠的状态。
最佳实践
先给我一个能一键运行、红绿分明的检查,再让我进循环。 具体四步:
- 先建可运行的检查设施。哪怕只是
npm test、pytest、cargo build、tsc --noEmit能一条命令跑起来。没有它,我后面所有的「自纠」都无从谈起——别因为嫌建测试设施麻烦就跳过这步,那等于拆了我的方向盘。 - 把验收标准编码成可执行断言,而不是留在聊天里。「价格不能为负」写成
assert parse("-1") raises,比写在需求文档里对我有用得多——前者我能跑,后者我只能「记得」。 - 让我「改→跑→看信号→再改」直到绿。明确告诉我:跑这条命令,把它跑到全过再回来。我会自己迭代,你不用每轮都来回。
- TDD 式:先写失败测试 ,再实现。让我先把测试写出来(包含边界用例)、你确认测试本身合理、再让我去让它们变绿。这样我无处藏拙,也不会反过来为了让测试通过而去改测试(信号必须是你定的,不是我能随手挪的)。
# 给我一个明确的循环指令,而不是一次性任务
你:用 pytest 把这些测试跑起来,迭代实现直到全部通过再停,
不要改测试本身;卡住就把失败输出贴给我。
示例
改之前(我只产出一次,你来当裁判):
你:实现一个限流器,每个用户每分钟最多 10 次请求。
我:(给出实现,读起来很合理)
你:(读了一遍,看着对,合并了)
线上:跨分钟边界时计数没清零,第 61 秒被误拦
改之后(我自己进了红绿循环):
你:先写测试:正常放行、第 11 次拦截、跨分钟窗口重置、并发同一用户。
我:(写出 4 条测试,跑一下——3 条红)
你:现在实现限流器,跑 pytest,迭代到全绿再停,别动测试。
我:(改→跑:2 红 → 改→跑:1 红 → 改→跑:全绿)
全部通过,跨分钟那条把窗口重置的 bug 逼出来了,已修。
差别不在代码本身,而在于第二次我带着信号在工作:那条「跨分钟」的红,是我自己看到并修掉的,没等到线上。
版本说明
「有可运行的检验时表现更好」是当前 agent 类编码工具的普遍特征,全版本、且跨模型适用。Claude Code 这类能自己执行命令、读取输出的 agent 尤其受益——它本来就具备「跑一下看结果」的能力,你要做的只是给它一个值得跑、且信号清晰的闭环。
延伸阅读与出处
- Claude Code Best Practices(Anthropic 官方) —— 官方建议在写代码前先让 Claude 写测试、确认测试失败后再实现,并把「能运行的验证」作为迭代的依据。
- My LLM codegen workflow atm(Harper Reed) —— 一套以测试驱动、让模型在可运行检查里迭代收敛的实战工作流。
- 反面对照见信任但不验证:没有这个闭环时,「看起来对」会怎样把你带进坑里。