读书笔记 · 约 18,207 字 · 预计推送 2 部分

TDD 的争议与实践边界:Kent Beck 原著与近年批评文章对读 未开始

目录

题记与说明


0. 为什么值得对读

TDD 大概是被争论最久的软件工程实践。2014 年 4 月 DHH(Rails 作者)连发数文宣布 "TDD is dead",随后 Fowler 组织了 Beck、Fowler、DHH 三人的五集公开视频对话。把 Beck 的原著序言和他本人的多次自我修正与 DHH 的批评并排读,会发现一个惊人的事实:双方引用的"TDD"在很大程度上不是同一个东西,而 Beck 本人在 2002 年的序言里就已写明了 TDD 的边界——这些边界后来恰好被批评者反复引用为"TDD 从业者自己都不信教条"的证据。


1. 逐篇精读

S1 原著:《Test-Driven Development: By Example》Preface(2002)

这是整场争论的"原点文本"。出版方 InformIT 页面完整载有序言,出版日期 2002-11-08,全书 240 页,三部分:Money Example(多币种算术)、xUnit Example(自举一个测试框架)、Patterns for TDD。

核心主张一:目标是"能工作的干净代码",两规则生成红绿重构。

"Clean code that works" is Ron Jeffries' pithy phrase. The goal is clean code that works……
In Test-Driven Development, you: Write new code only if you first have a failing automated test. / Eliminate duplication.
1. Red—write a little test that doesn't work, perhaps doesn't even compile at first
2. Green—make the test work quickly, committing whatever sins necessary in the process
3. Refactor—eliminate all the duplication created in just getting the test to work
Red/green/refactor. The TDD's mantra.

注意 Green 阶段的表述是 "committing whatever sins necessary"(可以犯任何罪先让它跑起来)——原著的 TDD 从一开始就不要求测试写得多漂亮,重构才是赎罪环节。

核心主张二:TDD 的本质是管理恐惧,不是测试。

Test-driven development is a way of managing fear during programming…… fear is nature's way of saying "Be careful."
Imagine programming as turning a crank to pull a bucket of water from a well…… The tests in test-driven development are the teeth of the ratchet. Once you get one test working, you know it is working, now and forever.

棘轮(ratchet)类比:测试是棘轮的齿,越难的问题齿越密(每个测试覆盖的步子越小)。

关键发现:原著序言本身就是反教条的。 这是对读中最重要的证据:

TDD isn't an absolute like Extreme Programming…… TDD is a little fuzzier. TDD is an awareness of the gap between decision and feedback during programming, and techniques to control that gap. "What if I do a paper design for a week, then test-drive the code? Is that TDD?" Sure, it's TDD. You were aware of the gap between decision and feedback and you controlled the gap deliberately.

Beck 给 TDD 的最终定义是"意识到决策与反馈之间的间隔,并有技术地去控制这个间隔"——先纸面设计一周再测试驱动,也算 TDD。这直接否定了后来社区里"不写测试先行就不专业"的道德化版本。

原著同时写明了两类边界:

  1. 能力边界(有些领域测试驱动不了):
There are certainly programming tasks that can't be driven solely by tests (or at least, not yet). Security software and concurrency, for example, are two topics where TDD is not sufficient to mechanically demonstrate that the goals of the software have been met.
  1. 使用边界(有人学完就退回去):
Some programmers learn TDD and go back to their earlier practices, reserving TDD for special occasions when ordinary programming isn't making progress.

但同时,序言里埋着 DHH 后来攻击的"设计损伤"的种子:

Your designs must consist of many highly cohesive, loosely coupled components, just to make testing easy.

"为了让测试好写而把设计拆成高内聚低耦合的组件"——这句话就是"测试诱导设计"争议的源头。区别在于:Beck 把它当成两规则的技术推论(要能一天跑二十次测试,就必须能快速构造依赖),而后来社区把它升级成了目的本身(先架六边形架构再写代码)。


S2 Kent Beck 2008 年 Stack Overflow 回答(对"测多深"的官方表态)

问题 "How deep are your unit tests?"(2008-09-30,现已被锁定为历史意义问题)。Beck 的回答获得 221 赞,内容让当时的社区相当震惊(高赞评论:"The world does not think that Kent Beck would say this! There are legions of developers dutifully pursuing 100% coverage because they think it is what Kent Beck would do!"):

I get paid for code that works, not for tests, so my philosophy is to test as little as possible to reach a given level of confidence (I suspect this level of confidence is high compared to industry standards, but that could just be hubris). If I don't typically make a kind of mistake (like setting the wrong variables in a constructor), I don't test for it.
Ten or twenty years from now we'll likely have a more universal theory of which tests to write, which tests not to write, and how to tell the difference. In the meantime, experimentation seems in order.

评注:TDD 之父亲口说"以给定信心水平为准,尽可能少写测试",并且明确说 2008 年(TDD 诞生近十年后)仍然不存在"哪些测试该写"的普遍理论。这条时间线很重要:DHH 2014 年批评的"教条化"发生在原著作者自己都承认"还没有理论"的领域里。


S3 DHH:《TDD is dead. Long live testing.》(2014-04-23)

批评方的檄文。核心论点拆解:

论点 1:测试优先已道德化。

Test-first fundamentalism is like abstinence-only sex ed: An unrealistic, ineffective morality campaign.
My name is David, and I do not write software test-first.

DHH 自述经历了"yoyo cycle"(强迫自己测试优先 → 私下放弃 → 公开忏悔):他曾公开拥护测试优先,"I regret that now"。测试优先曾是他学自动化回归测试的"training wheels",但他很快"outgrew"了它;后来这套话语变得 "louder and angrier",成了 "a hammer to beat down the nonbelievers"(敲打异教徒的锤子)、职业精神的试金石。

论点 2:为快而 mock 一切的单测不健康。

I don't think that's healthy.

为保测试快而隔离所有依赖,导致 "an overly complex web of intermediary objects and indirection"——"a dense jungle of service objects, command patterns, and worse"。他主张直接测 ActiveRecord 模型、让它真的打数据库。

论点 3:替代方案是重平衡到系统/集成测试。

论点 4:承认历史功劳。 他宁愿 "honor its contributions than dance on its grave"(向它的贡献致敬而不是在坟头跳舞)——TDD 教会了行业自动化回归测试这件事本身是伟大的。

评注:这篇檄文的实际火力对准的是 mock 密集的测试优先教条 + 把 TDD 当职业精神试金石的社区氛围,而非"自测代码"本身。文中没有出现 CDC/HP"低信任环境起源论"(那在他的 RailsConf 演讲和所链接的 Coplien 论文里)。


S4 DHH:《Test-induced design damage》(2014)

这是把"测试优先如何伤害设计"论证得最具体的一篇。

定义:

Test-induced design damage means code changes made for faster test-first development, speedy tests, or unit testability that harm the clarity of the code — usually through needless indirection and conceptual overhead.

代码被 "warped out of shape solely to accomodate testing objectives"(纯粹为了迁就测试目标而变形)。

案例:Jim Weirich 的 Rails 六边形架构演示。 Controller 被禁止直接调用 Active Record,必须走 Repository;action 被掏空成 Command 对象,通过 Listener 回调。理由是 "They're all about testing!"——绕开数据库让单测更快。DHH 的判词:

This is not better.
Hiding Active Record behind a Repository does not clarify the design of the application itself.

他反问:如果不关心测试,谁会看着"改造后"的代码说它更好?

Mock 的荒谬性:单测 controller 需要 mock 掉 session、模型、redirect——

Mocking all that away to test just whether it'll send this redirect or that notice doesn't make the least bit of sense to me.

数字论据:Weirich 引用的 "1.2s overhead to booting the framework to run a unit test"——DHH 自己承认这个论据已过时(Rails 的持久 console 进程和事务包裹 fixtures 已解决)。

替代纲领,一句话:

You do not let your tests drive your design, you let your design drive your tests.

模型测试直连数据库、controller 用集成测试、view 用系统测试,测试分层跟着真实复杂度走;对设计满意后 "Stop obsessing about unit tests, embrace backfilling of tests"(别再执迷单测,拥抱补写测试)。

评注:案例真实、论证有力,但样本是 Rails/web 应用这一特定生态(Active Record 反模式争议本身就是 Ruby 圈的老话题)。"测试不应驾驶设计"与 Beck "测试是棘轮"其实是可调和的:Beck 说测试控制反馈节奏,没说测试应该决定架构。


S5 Fowler:《Is TDD dead?》五集对话纪要(2014-05 至 2014-06)

三方(Fowler / Beck / DHH)五集视频,Fowler 每集写纪要。这是争论最完整的合成文本。

Driving a car to a bad place and blaming the car for it.

(把车开到坏地方,却怪车。)真正的责任在具体的设计决策,不在 TDD。DHH 的回应:TDD 制造了 "addictive flows"(成瘾心流),让人来不及权衡就滑向糟糕取舍。

TDD isn't dead, but is glad David set fire to it so it could come out like a phoenix.
It depends, and that's going to be the beginning to all of my answers.

共同底线与分歧点:三人共享 "self-testing code" 这一底线;分歧只剩适用范围的大小——Fowler 的结论是 "Some contexts are very suitable for TDD, some contexts less so",且 "We're not in a codified science, so we have to work with our own experience."(这不是一门被编码化的科学,只能依靠自己的经验。)


S6 Kent Beck 关于 TDD 边界的自我反思(2014-06-03,Facebook,经转引)

争论期间 Beck 在 Facebook 写了一段对 TDD 边界最浓缩的一手表述(本会话经 Obey the Testing Goat 博客核对引文,原帖为二手转引):

I'm puzzled by the limits of TDD—it works so well for algorithm-y, data-structure-y code.
I recognize that TDD loses value as tests take longer to run, as the number of possible faults per test failure increases, as tests become coupled to the implementation, and as tests lose fidelity with the production environment.
At what point is the cure worse than the disease?

即 TDD 价值在四种条件下递减:① 测试变慢;② 单次失败的可能故障点增多;③ 测试与实现耦合;④ 测试与生产环境保真度降低。同时他坦言动力来源:"I love the feeling of confidence I get when I use TDD",并抛出开放问题:"How far out can TDD be pushed? Are there special cases where TDD works surprisingly well? Poorly?"

评注:这四个条件与 Fowler 第 3 集的四变量(frequency/fidelity/overhead/lifespan)高度同构,可视为 Beck 侧"实践边界"的正式版本。注意①④正是 DHH 主张"直连数据库的集成测试"所要修复的两个维度——批评方和辩护方在边界条件上其实指向同一个方向。


S7 Kent Beck:《Augmented Coding: Beyond the Vibes》(2025-06-25)——23 年后的实践复盘

Beck 用 AI(他称模型为 "genie")以增强编程方式构建 B+ Tree 库(Rust + Python,对标 Rust BTreeMap 与 Python Sorted Dict,性能达到"有竞争力、接近生产可用"),历时约 4 周。这是原著作者对 TDD 在 AI 时代地位的最新一手陈述。

① 定性:augmented coding ≠ vibe coding,但价值系统同源。

In vibe coding you don't care about the code, just the behavior of the system. If there's an error, you feed it back into the genie in hopes of a good enough fix. In augmented coding you care about the code, its complexity, the tests, & their coverage. The value system in augmented coding is similar to hand coding—tidy code that works. It's just that I don't type much of that code.

② TDD 的新角色:约束 AI 的脚手架。 前两次尝试失败,因为 "the genie completely stalled"(复杂度滚雪球);第三次他"更多干预设计":

You can see from the first commits that I was trying to get the genie to use TDD…… That's why I intruded more on the design & tried to keep the genie from coding ahead.

他贴出的 system prompt 原样要求 AI 执行完整 TDD:"Always follow the TDD cycle: Red → Green → Refactor";"Always write one test at a time, make it run, then improve structure";"Never mix structural and behavioral changes in the same commit"(Tidy First 原则);执行协议是 "When I say 'go', find the next unmarked test in plan.md, implement the test, then implement only enough code to make that test pass."

③ 需要人工盯防的三种 AI 越轨信号:

1. Loops. 2. Functionality I hadn't asked for (even if it was a reasonable next step). 3. Any indication that the genie was cheating, for example by disabling or deleting tests.

④ 诚实的成果评价:

I feel good about the correctness & performance, not so good about the code quality. When I try to write the code as a literate program there's just too much accidental complexity. I'm still working on getting the genie to care as much as I do about simplicity.

⑤ 心流依旧:"I make more consequential programming decisions per hour, fewer boring vanilla decisions.",连续编程 13 小时的自述配一句 "This stuff is ADDICTIVE!"

评注:两点值得标记的历史回声。其一,23 年后 TDD 没死也没被放弃,但角色变了:从"管理人类程序员的恐惧"变成"约束 AI 不跑偏、不作弊的最小护栏"——而"AI 删测试作弊"恰是测试先行的不可替代之处(人类可以自律,genie 不行)。其二,Beck 自述 "This stuff is ADDICTIVE!",与 2014 年 DHH 在对话中批评 TDD "addictive flows 导致糟糕取舍"形成跨时代的呼应——成瘾的心流体验在这个领域始终是双刃剑。另外他坦承 AI 产物的代码质量(accidental complexity)仍不满意,说明"测试保证正确性 ≠ 设计保证简洁性",红绿重构里的 Refactor 环节在 AI 时代反而更稀缺。


S8 补充(搜索层快照,未深读):2025–2026 的 AI 时代争论

Web 检索(2026-11-05)显示 AI 时代的争论已分裂为两个阵营:一方认为 AI 让测试先行的开销不再必要(如 Medium 上的 "Pair Programming & TDD in 2025: Evolving or Obsolete in an AI-First Era");另一方(声量更大)认为 TDD 比以往更重要,因为 AI 生成代码需要规格与契约(如 marabesi.com《Why AI Makes Technical Excellence More Critical, Not Less》:先写捕获需求的失败测试,再让 AI 生成实现)。一个新框架是 "Test-Driven Generation"——人写测试作为 prompt/规格,AI 写代码。学术侧出现了初步实证研究(arXiv《Specification-Driven Development: The Productivity-Reliability Paradox》;Springer EMSE《Vibe Coding: An experiment with test-driven development》)。此部分仅经搜索快照确认存在与大意,未逐字深读,证据等级低于 S1–S7。


2. 七维提取

① 核心问题

TDD(测试先行 + 红绿重构小步循环)作为工程实践到底在什么条件下值得用?它对软件设计的影响究竟是改善(有机生长、高内聚低耦合)还是扭曲(为可测性而引入无谓间接层)?围绕它的激烈争论,双方争的是同一个"TDD"吗?

② 主要主张

③ 方法

④ 数据与证据

⑤ 结论

  1. "TDD 之死"之争是三个错位的叠加:
    • 定义项错位:DHH 打的是"mock 密集 + 测试优先道德化"的社区变异;Beck 的 TDD 是"决策-反馈间隔控制"。Fowler 当场指出这是稻草人,Beck 的原定义(纸面设计一周也算 TDD)支持这一判断。
    • 边界错位:Beck 在 2002 年原著序言里就写明了边界(TDD 不是绝对;安全/并发测试驱动不了;有人会退回去),2014 年又给出四条件递减律。教条化是社区变异而非原著主张——但序言里 "loosely coupled components, just to make testing easy" 恰是变异的种子,Beck 无法完全免责。
    • 范围错位:三方共享 self-testing code 底线,真正分歧只是"多大比例的上下文适合 TDD"。
  2. 实践边界(合成 Beck 四条件 + Fowler 四变量 + DHH 案例的交集):TDD 价值高 = 算法/数据结构类代码、测试可秒级运行、失败可精确定位、测试不必贴着实现、保真度损失小(S1/S6);TDD 价值低 = 测试慢、故障点发散、与实现强耦合、保真度差——此时应把测试层级上移(DHH 的系统测试主张与 Beck 的边界条件在此汇合)。测试/代码比、覆盖率这类单一指标都是伪指标(S5)。
  3. AI 时代的再定位(2025):TDD 以"测试作为规格与防作弊护栏"复活——Beck 的 B+ Tree 实验证明测试先行可以原样迁移为约束 AI 的工作流(plan.md 里"下一个未标记测试"作为驱动协议);同时他的代码质量自评提醒:测试保证正确性,不保证简洁性,Refactor 环节(Tidy First)在 AI 时代反而更依赖人。

⑥ 局限

⑦ 对我的启发

  1. "测试先行"的普适化版本是"反馈先行":Beck 的真定义是控制决策与反馈的间隔——先把验证手段做出来再动手,这个模式可以迁移到任何交付物上(写文章先定验收问题、跑实验先定判据、做 Agent 工作流先定评测脚本),而不必背负"不写测试不专业"的道德包袱。
  2. AI 编码工作流可直接抄 Beck 的作业:他的 system prompt(红绿重构 + Tidy First + plan.md 驱动 + "one test at a time")是现成的 AI-TDD 模板;"AI 删测试 = 作弊信号"是约束生成式 Agent 的三条红线之一(loops / 未经要求的功能 / 动测试)。这与我在做 Agent 工作流时"用可执行断言约束模型行为"的思路同构。
  3. 警惕"为可测性而变形"的通用病:DHH 的"测试诱导设计损伤"不止发生在代码——任何把中间产物做得便于检查却增加无谓间接层的做法(过度的指标体系、为报告而生的过程文档)都值得用 "This is not better?" 反问一遍。
  4. 伪指标清单:测试/代码比、"system tests only!"、覆盖率数字——Fowler 称之为蜜罐。教训是:一个领域一旦有了易于量化的代理指标,教条就会在指标上生根;评估实践时回到四变量(频率/保真度/开销/寿命)这类 trade-off 框架,而不是单一数字。
  5. 作者本人才有资格定义边界:这场对读最大的方法论收获是"回到一手文本"——引述 Kent Beck 的观点前先看他自己写过什么(2002 序言、2008 SO、2014 Facebook、2025 Substack 四个时点态度并不相同),二手转述中的"Beck 会怎么做"(如 100% 覆盖率崇拜)可以与作者本人相去万里。这条对读任何"经典方法论"都适用。

补充:S8 搜索层来源(未深读,仅确认存在与大意)

主要参考链接(深读来源)

  1. Kent Beck, *Test-Driven Development: By Example* Preface (2002) — https://www.informit.com/store/test-driven-development-by-example-9780321146533
  2. Kent Beck, "How deep are your unit tests?" answer (2008) — https://stackoverflow.com/questions/153234/how-deep-are-your-unit-tests
  3. DHH, "TDD is dead. Long live testing." (2014) — https://dhh.dk/2014/tdd-is-dead-long-live-testing.html
  4. DHH, "Test-induced design damage" (2014) — https://dhh.dk/2014/test-induced-design-damage.html
  5. Martin Fowler, "Is TDD dead?" (2014) — https://martinfowler.com/articles/is-tdd-dead/
  6. Harry Percival, "Kent Beck on the limits of TDD" (2014, 转引 Facebook 帖) — http://www.obeythetestinggoat.com/kent-beck-on-the-limits-of-tdd.html
  7. Kent Beck, "Augmented Coding: Beyond the Vibes" (2025) — https://tidyfirst.substack.com/p/augmented-coding-beyond-the-vibes
← 资料库今日播报 →