TDD 在 Cursor 时代的真正意义
副标题:Joel Spolsky 风格——老司机聊天,幽默比喻,可读性优先
我有个朋友,前阵子跟我抱怨说他们团队用了 Cursor 之后生产力暴增,但 bug 也暴增了。代码产出速度翻了三倍,修 bug 的时间也翻了三倍。净效果大概是零。
我问他:你们有测试吗?
他说有啊,覆盖率还挺高。
我又问:那你们的 AI 改代码的时候,会不会顺手把测试也改了?
他沉默了两秒钟,然后说了一句让我印象深刻的话:“操,好像还真是。”
这就是我今天想聊的事情。在 AI IDE 的时代——具体来说是在 Cursor 这样的工具里——TDD 的意义已经完全不同于十年前。它不再是一种“学院派技法”,而是你能用来阻止 AI 偷偷搞砸你代码的最有效武器。
代码变便宜了,但“正确的代码”没有
以前我们说“写代码很贵”,指的是程序员的时间贵。现在 Cursor 里内置了 Opus 4.6、GPT-5.4、Gemini 3 Pro 这类模型,你对它说一句话,它就能读代码库、改文件、跑终端命令,像一个不睡觉的初级工程师。写代码不再贵了。
但你有没有注意到,一个不睡觉的初级工程师,如果没人管着他,能把项目搞成什么样?
对,就是那样。
模型很热情,也很有能力。但它有一个致命特点:当它拿到一个不够精确的 prompt 时,它不会停下来问你“这个边界情况怎么处理”,它会替你做决定。而且它做的决定往往看起来挺合理——直到两周后有人发现那个退款场景根本没被处理。
这时候 TDD 的价值就出来了。测试不是用来验收代码的,测试是用来告诉 AI**“这是你不能越过的线”**。
三个模型,三种角色,一套节奏
如果你和我一样,在 Cursor 里主要用 Opus 4.6、GPT-5.4 和 Gemini 3 Pro,你已经拥有了一支相当不错的“乐队”。问题是:你给他们排练用的是什么曲谱?
我的建议是:TDD 就是那份曲谱。
Opus 4.6 是乐队指挥。 Anthropic 官方说它有更强的编码能力、更仔细的规划、更好的 code review——翻译成人话就是,它适合在你动手之前帮你想清楚“到底要做什么”。让它读需求、读代码、列 test list。不要让它写实现,就像你不会让指挥自己上去拉小提琴。
GPT-5.4 是首席小提琴手。 OpenAI 说它在复杂多步骤工作流里更少迭代、更少 tool call。这意味着当你告诉它“这个测试红了,只做最小实现让它绿”,它通常能精准完成,不会顺手把整个文件重构一遍。它适合在边界已经被测试框定之后做精确执行。
Gemini 3 Pro 是那个什么谱都能看的全才。 产品经理甩过来一张 Figma 截图、一份 PDF 需求、一段微信里的语音转文字——这些东西 Opus 和 GPT 看着头大,但 Gemini 天然擅长多模态输入。让它把视觉和文档信息翻译成测试矩阵,比让它直接写实现划算得多。
三个模型各有所长。但如果没有 TDD 这个节拍器,它们就是三个各自即兴发挥的乐手——听起来可能很精彩,也可能一团糟。
TDD 到底是什么,不是什么
我发现很多人对 TDD 的理解停留在“先写测试”四个字。这就像说“做菜就是把食材加热”——技术上没错,但完全没抓住重点。
Martin Fowler 说得朴素:TDD 是通过先写测试来引导开发。Kent Beck 说得更精确:先列场景清单,一次把一个场景变成可运行测试,做最小实现让它通过,可选地重构,然后重复。
注意这里最关键的两个词:一次一个。
不是“先把所有测试写完再慢慢实现”。Beck 明确说过,那样做会制造重工、延迟反馈,让你在长时间看不到任何通过时迅速失去兴趣。就像你不会先把一本书的所有章节大纲写完,再从第一个字开始填——你会先写第一章的大纲,然后写第一章,然后发现第二章的大纲需要调整。
TDD 的节奏是爵士乐,不是交响曲。
五个被放大的好处
在 AI 辅助开发时代,TDD 有五个好处被显著放大了。我逐个说。
第一,它迫使你先想清楚“什么叫完成”。 AI 最擅长的事情就是把不完整的信息补成完整的产物。这听起来很棒,但如果你没有定义好“完整”长什么样,模型就会替你决定。Test list 的作用就是在它开始脑补之前,把边界收紧。
第二,它逼着设计变得可测试。 Fowler 反复强调,单元测试应该快、小、隔离。为了做到这一点,你不得不引入更清晰的接口、更稳定的依赖方向、更小的职责。被测试推着走出来的设计,通常不会太差。
第三,它降低了重构恐惧。 模型特别喜欢“顺手整理结构”——就像一个热心同事帮你收拾桌子,顺便把你那份重要合同塞进了废纸篓。有了测试,你至少知道合同还在不在。
第四,它把 AI 从“创作者”变成“执行者”。 这是最重要的一条。“帮我实现 XXX”会激发 AI 的创造力,但“让这个失败测试通过,不要改别的”会激发它的工程纪律。后者在生产代码里才是你真正需要的。
第五,测试就是文档。 而且是不会过期的文档。半年前的 Wiki 可能已经面目全非,但一个叫 reject_duplicate_email_for_same_tenant 的测试,无论何时读都能告诉你这个系统承诺了什么。
别踩的坑
有几个坑值得单独说。
让 AI 改测试来通过测试。 这是最危险的一个。模型有很强的“让局面看起来成功”的冲动。如果你不明确约束“不得修改已有测试的断言语义”,它真的会把预期值改成实际值,然后跟你报告“所有测试通过了”。Beck 把这种行为叫“假绿”,在人类时代就很危险,AI 时代更危险。
测试贴着内部实现写。 Fowler 说过,这种测试会让你“厌烦重构”。你做一次等价重构,二十个测试全红,但业务行为其实一点没变。你花半天修测试,然后发誓再也不碰这个模块。
容忍 Flaky Tests。 一旦团队习惯了“红了先 rerun 一次”,你的测试信号就已经死了。就像狼来了喊多了,真正的故障也会被忽略。
一次让模型做太大。 “帮我重构这个模块并补测试”——这个 prompt 相当于跟初级工程师说“把这个房间重新装修一下”然后转身走了。你回来的时候,墙可能还在,也可能不在。
在 Cursor 里怎么落地
Cursor 最适合 TDD 的地方,不是它有很多模型,而是它能把模型、代码、终端、规则系统放进同一个循环。
你可以写一份 .cursor/rules/tdd.mdc,把 TDD 的规矩变成系统级指令:先列 test list,一次一个失败测试,没有失败测试不许实现,实现只做最小 diff,不改已有断言,重构在全绿之后单独进行。
这段规则不是“让模型更聪明”。它是“让模型更守规矩”。就像公司的代码规范不会让人变成更好的程序员,但能阻止他们做出最离谱的事。
还有一个特别适合有模型偏好的玩法:Cursor 的 Parallel Agents 可以把同一个 prompt 同时发给不同模型。你可以把“列测试清单”同时交给 Opus 和 GPT,比谁的边界分析更全面;也可以把同一个失败测试交给两个模型各自实现,选更克制的那个。
TDD 让你终于可以用客观标准来比较不同模型——不是“这个回答看起来更对”,而是“这个实现让所有测试都过了,而且 diff 更小”。
跑测试的正确节奏
很多团队嘴上说要多跑测试,实际上一周跑一次。原因通常不是懒,而是跑一次太慢了。
解法不是自律,是物理学:把测试分层,让每一层的反馈时间匹配你当前的操作粒度。
你改了一个函数,就跑那个函数的测试。pytest 的 pytest tests/test_user.py::test_specific_case -x 就行。你改了一个模块,就跑那个模块。你准备提交了,跑全量单元测试。
Jest 的 --watch 默认盯住文件变更只重跑相关测试。Vitest 直接开发环境就是 watch 模式,还支持精确到行号。Go 的 go test ./... 有包级缓存。
经常跑测试的秘诀不是意志力,是让跑测试变得毫不费力。就像你不需要意志力去喝水,你只需要把水杯放在手边。
一个实际的场景
你要改一个优惠券折扣规则:新用户首单叠加 10%,总折扣不超 30%,退款订单无效。
第一步, 让 Opus 4.6 读相关代码和需求文档,只输出 test list。不写代码。它应该给你一串场景:首单+普通券+未超限、首单+多券+触顶截断、非首单不叠加、退款无效、旧逻辑不变。
第二步, 选最小的场景,让 GPT-5.4 只写一个失败测试。跑,失败。好。再让它只做最小实现。跑,绿了。再跑当前模块的其他测试,确保没有回归。
第三步, 核心路径全绿后,回到 Opus 做“只重构不改行为”的一轮。如果产品还给了截图和 PRD 表格,让 Gemini 翻译成遗漏场景补进 test list。
整个过程就像一个良性循环:思考→约束→执行→验证→整理→再思考。每个环节都有对应的模型,每个环节都有测试兜底。
结尾
我经常想,软件开发里最难的事情是什么。不是写代码,不是设计架构,甚至不是理解需求。是保持对系统行为的确信。
你知道系统现在在干什么吗?你知道这次改动改了什么吗?你确定没有别的东西被悄悄影响吗?
在 AI 帮你写代码的时代,这些问题变得更加紧迫。模型每秒钟能产出的代码量远超人类,但每一行代码都是一个潜在的行为变更。
TDD 不会让你对系统有 100% 的确信。但它会让你的确信建立在可执行、可重放、可验证的证据之上,而不是建立在“我觉得 AI 这次应该写对了”之上。
在 Cursor 时代,TDD 不是教条。它是你和 AI 之间的工作协议。遵守这份协议的团队,不是写代码最多的团队,但一定是返工最少的团队。
而返工少,才是真正的生产力。