你大概率经历过这个场景:代码的行为一点没变,只是重命名了一个方法、抽了一个函数、调整了一下内部结构,跑一遍测试,满屏飘红。团队复盘,最后往往得出一个结论——“这代码动不得,重构成本太高”。于是测试从“变更的安全网”,悄悄变成了“变更的手铐”。
十年前,英国资深工程师Ian Cooper在NDC伦敦大会上做了一场约54分钟的演讲《TDD, Where Did It All Go Wrong》(TDD到底是在哪一步走歪的),正面回答了这个让无数团队破防的问题。他的核心判断很扎心:不是TDD失效了,而是我们把“单元测试”的“单元”测错了对象——Kent Beck当年说的unit是“行为单元”,后来被我们偷偷换成了“一个类”。这场演讲至今仍是测试主题下被引用最多的长视频之一,无论你写Python、Go还是做网络自动化脚本,它都能帮你把测试这件事的底层逻辑理顺。
| 项目 | 内容 |
|---|---|
| 视频标题 | TDD, Where Did It All Go Wrong |
| 频道 | NDC Conferences |
| 时长 | 约54分钟 |
| 播放量 | 近百万级,测试主题常青经典 |
| 核心主题 | TDD的初衷、走歪的过程与回归路径 |
| 演讲者 | Ian Cooper,英国资深工程师,开源消息框架 Brighter 作者 |
Cooper开场的姿态不是布道,而是“验伤”。他描述了一个几乎所有团队都遇到过的困境:TDD承诺的图景是“放心大胆地改代码,测试兜底”;可现实是,真正需要测试兜底的时刻——重构——恰恰是测试最先报警的时刻。行为没变,测试红了;你修的不是产品代码,而是几百个测试用例。(此处为演讲内容的大意转述。)
我的延伸分析:这个开场之所以击中人,是因为它指出了一种责任倒置。TDD的本意是“测试因业务行为失败,所以你改代码”;走歪之后变成“测试因内部结构变化失败,所以你不敢改代码”。前者测试在保护业务,后者测试在保护代码的形状。一旦团队长期处于后者,对“写测试”这件事的信任会整体崩塌——很多人由此得出“TDD没用”的结论,其实他们从没做过真正的TDD。
演讲用了相当篇幅做“考古”。TDD并非2000年代的新发明,它源自Smalltalk时代的开发传统,由Kent Beck整理复兴,最终沉淀在2002年的经典著作《Test-Driven Development: By Example》里。Beck的本意是:先写一个表达行为的失败测试,再写恰好让测试通过的代码,然后重构——这就是红-绿-重构循环。测试在这里首先是“设计与规格工具”,其次才是回归防线。
关键在“unit”这个词。Beck语境里的unit test,测的是unit of behavior(行为单元)——一个用户可感知的功能切片。Cooper特别强调:行为单元的粒度可以很大,大到“用户提交订单”这个层面,完全不必落到每个类每个方法。(大意转述。)
我的延伸:这一点很多中文资料从未讲透。“单元”不是解剖学意义上的最小零件,而是需求意义上的最小承诺。理解了这一层,后面所有的争论才有裁判标准。
那到底是在哪一步歪的?Cooper给出的答案是2000年代中期测试框架与mock框架(Moq、Mockito、Rhino Mocks这一批)大爆发之后:社区把“unit test”的unit默认理解成了一个类,再配上mock,把被测类的一切协作者都替掉,美其名曰“隔离”。
他把两大流派摆上台面:经典派(Detroit风格,真实对象+状态验证)与伦敦派(London风格,mock协作者+交互验证)。在Feathers著名的定义里(出自2004年《修改代码的艺术》),一个测试只要碰了网络、数据库、文件系统,就算integration test而非unit test——这条定义被普遍误读成了“必须mock掉一切外部协作”。(大意转述。)
我的延伸:这里要替伦敦派说句公道话——伦敦派自己的成熟形态(Freeman与Pryce的《测试驱动的面向对象软件开发》)主张的是由外向内分层驱动设计,并非“每个类都配上满堂mock”。Cooper批判的是mock被滥用后的民间形态,不是流派本身。这个nuance视频里点到即止,读者自行延伸阅读时值得留意。
测试焊死实现细节之后,Cooper列出的账单相当难看,我归纳为四类:
假警报:重构(行为不变的结构调整)必然导致测试变红。红-绿-重构的第三步“重构”直接瘫痪,只剩红和绿,代码质量迭代通道被堵死。
过指定:mock验证的是“你有没有按我预期的顺序、以我预期的参数调用了协作者”,这是在验证实现路径而非结果。换一种同样正确的实现,测试照样红。
慢与脆:内部每个协作者都要搭建替身,Arrange部分越写越长;一个内部改名,几十个测试连锁报错。
信任破产:当维护测试的成本超过收益,团队开始删测试、跳过测试、“走过场式”地写测试。最后不是TDD死了,是测试文化死了。
我的延伸:这里面最隐蔽的是测试债务比代码债务更难被发现。代码坏了会报错,测试“坏了”(测错了对象)不会报错——它只是让你的每一次正确改动都显得像事故。
整场演讲最重要的产出,是一个可以立刻上手的判据:行为不变而测试变红,错的是测试,不是代码。
Cooper给出的自查方式很朴素:问自己“这个测试的失败,能不能被业务方理解?”如果测试失败的原因是“方法被改名了”“内部调用顺序变了”,业务方只会一脸茫然——那你测的就是结构。(大意转述。)反过来,好的测试失败时,理由应该是一句业务语言:“下单没有扣库存”“账单金额算错了”。
我的延伸:这条判据其实给了“该改测试还是该改代码”一个清晰裁决。很多团队在红灯面前的争论,本质是缺这条判据:行为错了→改代码;行为没变而测试错了→改测试,且要追问当初为什么写出这种测试。两件事混在一起,红灯就失去了信息量。
怎么测才不焊死结构?Cooper的答案是沿模块的公共API测——把“单元”重新定义为行为单元,测试从模块对外的端口打进去,把它当成一个黑盒:给它输入,断言输出与副作用,不关心内部怎么实现。
支撑这一做法的架构抓手是六边形架构(Hexagonal Architecture,Cockburn提出):把业务逻辑放在核心,通过端口与适配器和UI、数据库、消息队列打交道。这样一来,业务逻辑可以在“无界面、无数据库”的头里跑测试——不慢、不脆,而且天然只暴露行为。(大意转述。)
我的延伸:这不是让你放弃分层测试,而是调整默认档位。外圈放少量故事级测试(story test)守住业务承诺,内圈的测试只在确实需要时才写。多数团队的问题恰恰是倒置的:内圈几百个类级测试密不透风,外圈几乎没有。
关于mock,Cooper的纪律可以浓缩成两句话:只mock边界上的东西;优先验证状态而不是验证调用。
边界是什么?你的代码与“你不拥有的世界”交界处:网络、数据库、消息总线、文件系统、时钟这类慢的、不确定的、测试环境里不可控的东西——在端口处用替身换掉它们。而模块内部的协作对象,尽量用真实对象,跑完了断言最终状态,而不是检查“你有没有调用过它三次”。他还引用了一个流传很广的原则:别mock你不拥有的类型,第三方库的类不该成为你替身体系的地基。(大意转述。)
我的延伸:这条纪律的价值在于把“隔离”这个词的意思修正了。隔离的目的从来不是“把类关进真空罩”,而是把不确定性关在门外。真实对象并不可怕,不可控、慢、会漂移的东西才可怕。想通这一点,mock的使用量通常会降一个数量级,测试的可信度反而上升。
flowchart TD
A[TDD的岔路口] --> B[原意:行为驱动]
A --> C[走歪:结构驱动]
B --> D[unit即行为单元]
B --> E[红绿重构循环]
B --> F[测试是安全网]
E --> G[重构须保持全绿]
C --> H[unit缩成单个类]
C --> I[内部协作全mock]
H --> J[测试焊死实现]
I --> J
J --> K[重构就亮红灯]
J --> L[测试沦为负债]
K --> M[没人敢动代码]
L --> N[删测试走过场]
M --> O[代码逐渐腐烂]
N --> O
A --> P[回归正道]
P --> Q[沿公共端口测]
P --> R[替身只留边界]
P --> S[六边形架构托底]
Q --> T[重构自由]
R --> T
S --> T
看完这场演讲,建议你本周就对自己的测试套件做一次“三问体检”:
第一问:我敢不敢重构? 挑一段想优化的代码,只做结构调整、不改行为,跑测试。如果红了,别急着修——先判断红灯归属:行为真的变了吗?没有,那就是测试在测结构,把这些测试视为债务登记下来。
第二问:我的测试失败时,说的是业务话还是代码话? 把每个失败信息读一遍。如果失败原因是“某方法不存在”“某mock未被调用”,它就是过指定测试;如果失败原因是“金额算错”“状态没流转”,它才是行为测试。
第三问:我的mock有没有越界? 数一数替身的对象:如果大量替身是自家模块的内部类,替身纪律已经失守;替身应该集中在端口上——数据库、网络、消息总线、时钟。
对通信行业的读者多说一句:做核心网自动化、信令仿真脚本时,这套思想同样直接适用——把网元接口、SBI调用、时钟当成端口和替身,把签约判定、话单校验这类业务逻辑写成不碰网络的“函数式核心”,测试速度和稳定性都会上一个台阶。这个迁移是我基于演讲原则的延伸,不是视频原内容。
一句话概括行动指南:下一个测试,从模块的公共API开始写,从最像业务承诺的那条路径开始写。
测试是行为的安全网,不是结构的封条——对象测错了,再自律的TDD也会把团队逼上删测试的不归路。
本文由「51学通信」技术团队出品。51学通信由通信行业18年老兵爱卫生(微信ID: gprshome201101)维护,聚焦5G/6G核心网、3GPP标准解读、通信前沿技术深度分析。 出版作品有《5G核心网原理与实践》《IMS原理与实践》等4本通信专业书籍。知识星球「51学通信」提供深度技术答疑和行业洞察。
[!tip] 51学通信知识星球 星主:爱卫生(微信:gprshome201101)——公众号「51学通信」创办者,19 年通信行业运维与讲师经验,著有《GPRS网络信令实例详解》《EPC原理与实践》《5G核心网原理与实践》三本纸质书(各大电商平台有售)。 星球定位:深耕 5G 核心网、IMS、NFV/AI、云原生等 ICT 领域,主打运维岗位与信令分析。 当前规模:5200 篇主题 / 990 个回答,198 元/年。 加入福利:加入星球后请加爱卫生微信,拉您进知识星球年度陪伴与答疑群——不限量回复您的各种问题,另有群内分享。 AI 加持:51学通信知识星球已作为 skill 接入 WorkBuddy,使用 deepseek-4-flash 在海量星球帖子中精准找贴、找视频、找 FAQ 和案例,做总结、找文档和 PPT 等场景全覆盖。
