Chapter 3:Test-Driven Development Foundations
对齐原书 Chapter 3:理解单元测试和 TDD 基础、红绿重构、TDD 三条规则、从红到绿的最小实现策略,以及让个人和团队成功采用 TDD 的心态与机械习惯。
学习目标
- 能用 TDD 三条规则约束测试与生产代码的切换,识别有效红灯、环境伪红灯和过度实现
- 能从一个失败例通过伪实现、显而易见实现或三角测量得到最小绿灯,再在全绿下重构
- 能建立测试清单、短运行入口和失败恢复纪律,把 TDD 从个人技巧变成可持续的团队反馈机制
机制总览
Chapter 3:Test-Driven Development Foundations:机制路径
- 1
从示例上升为可重复的工作协议
上一章展示了 Soundex 的连续小步。本章回答更一般的问题:什么算单元测试,红绿重构为何必须按顺序,三条规则如何限制步幅,卡在红灯时怎样前进,以及团队为何常在“测试太慢”“需求太急”时放弃纪律。
- 2
TDD 是设计反馈,不是测试数量竞赛
TDD 的直接产物是测试,但主要价值是更早暴露接口难用、责任混合和依赖不可控。先站在调用者角度写示例,会迫使代码在实现之前回答:输入怎样表达,结果怎样观察,失败怎样报告,协作者怎样替换。
- 3
条规则把步幅压缩到可诊断范围
TDD 三条规则各自限制一种过量变化:第一条阻止无需求证据的生产代码;第二条阻止一次写出大量测试;第三条阻止为了想象中的未来提前一般化。它们不是法律,也不禁止思考,而是让每次变化足够小,以便失败时能立即指出原因。
章级决策实验
Chapter 3:Test-Driven Development Foundations:机制与证据
切换《Chapter 3:Test-Driven Development Foundations》的三个关键教学阶段,先解释机制,再用运行与失败证据验证结论。
选择推理阶段
当前阶段 · 从示例上升为可重复的工作协议
上一章展示了 Soundex 的连续小步。本章回答更一般的问题:什么算单元测试,红绿重构为何必须按顺序,三条规则如何限制步幅,卡在红灯时怎样前进,以及团队为何常在“测试太慢”“需求太急”时放弃纪律。
可核验证据
保存红—绿—重构的最小提交与失败消息,用行为断言、替身交互和重复运行核对「从示例上升为可重复的工作协议」是否提供快速反馈。
学完《Chapter 3:Test-Driven Development Foundations》后,应能从输入和前置条件推导状态变化,并用可重复的构建、运行或边界测试证明结果。
失效—证据矩阵
Chapter 3:Test-Driven Development Foundations:失效与核验
从示例上升为可重复的工作协议
典型失效
若把「从示例上升为可重复的工作协议」退化成先写实现再补断言,测试会耦合内部步骤,既不能驱动设计,也无法稳定解释失败。
核验证据
保存红—绿—重构的最小提交与失败消息,用行为断言、替身交互和重复运行核对「从示例上升为可重复的工作协议」是否提供快速反馈。
TDD 是设计反馈,不是测试数量竞赛
典型失效
若把「TDD 是设计反馈,不是测试数量竞赛」退化成先写实现再补断言,测试会耦合内部步骤,既不能驱动设计,也无法稳定解释失败。
核验证据
保存红—绿—重构的最小提交与失败消息,用行为断言、替身交互和重复运行核对「TDD 是设计反馈,不是测试数量竞赛」是否提供快速反馈。
条规则把步幅压缩到可诊断范围
典型失效
若把「条规则把步幅压缩到可诊断范围」退化成先写实现再补断言,测试会耦合内部步骤,既不能驱动设计,也无法稳定解释失败。
核验证据
保存红—绿—重构的最小提交与失败消息,用行为断言、替身交互和重复运行核对「条规则把步幅压缩到可诊断范围」是否提供快速反馈。
从示例上升为可重复的工作协议
上一章展示了 Soundex 的连续小步。本章回答更一般的问题:什么算单元测试,红绿重构为何必须按顺序,三条规则如何限制步幅,卡在红灯时怎样前进,以及团队为何常在“测试太慢”“需求太急”时放弃纪律。
↡在隔离且可控的条件下,快速验证一个窄行为单元并给出确定结果的自动化检查。“单元”不等同于一个函数或一个类。它是测试能够快速、确定地观察的行为边界:可能是一个纯函数,也可能是几个对象组成的小协作。若测试依赖真实网络、共享数据库或墙上时钟,它仍可能有价值,但反馈速度、隔离性与失败定位已经不同。
TDD 是设计反馈,不是测试数量竞赛
↡用一个正确失败的自动化示例决定下一条生产行为,再通过最小实现和持续重构推进设计的开发方法。TDD 的直接产物是测试,但主要价值是更早暴露接口难用、责任混合和依赖不可控。先站在调用者角度写示例,会迫使代码在实现之前回答:输入怎样表达,结果怎样观察,失败怎样报告,协作者怎样替换。
测试很多不等于采用 TDD。若先实现完整功能再补覆盖,测试提供回归保护,却没有参与设计。反过来,先写一个巨大的端到端测试,再开发数天才变绿,也失去了短反馈回路。
Red-Green-Refactor(红绿重构)是一条有顺序约束的反馈协议:先让一个目标明确的例子按预期失败,再用最小改动取得绿灯,最后只在全绿保护下改善结构。若跳过红灯,就无法证明测试真的观察到目标行为;若在绿灯前重构,就会把行为缺口与结构变化混在同一次失败里。
三条规则把步幅压缩到可诊断范围
↡约束 TDD 工作顺序的三项纪律:只为失败测试写生产代码,只写足以失败的测试,只写足以通过的生产代码。TDD 三条规则各自限制一种过量变化:第一条阻止无需求证据的生产代码;第二条阻止一次写出大量测试;第三条阻止为了想象中的未来提前一般化。它们不是法律,也不禁止思考,而是让每次变化足够小,以便失败时能立即指出原因。
例如空栈行为尚不存在时,测试可以先因 Stack::isEmpty 未声明而编译失败。按第二条规则,这已经足够红;先只添加接口让测试继续执行,然后观察值断言失败。一次同时添加 push、pop、迭代器和动态扩容,会让下一批测试失去设计压力。
TEST(StackTest, IsEmptyWhenCreated) {
Stack<int> stack;
EXPECT_TRUE(stack.isEmpty());
}红灯必须证明测试有能力失败
↡测试因目标行为尚不存在或不正确而失败,并且失败位置、消息和差值与事前预测一致的状态。直接写测试然后立即实现、从未看见失败,会漏掉断言写反、测试未注册、路径未执行等问题。正确红灯至少包含:新测试被发现,旧测试原本全绿,失败只指向目标缺口,输出能解释预期与实际。
ctest --test-dir build -R StackTest --output-on-failure
ctest --test-dir build --output-on-failure第一条提供局部快反馈,第二条验证没有破坏其他行为。局部过滤不能成为永久默认,因为它会让未运行测试伪装成全绿。
从红到绿有三种常用策略
↡只满足当前失败例且不提前加入未被测试要求行为的最小生产代码。伪实现先返回常量,让接口与断言连通;显而易见实现在解法简单且风险低时直接写清楚逻辑;三角测量用第二个有区分力的例子推翻常量,迫使实现一般化。选择哪一种取决于不确定性,而不是技术炫耀。
// 第一例只要求新栈为空,最小状态足够。
bool isEmpty() const {
return elements_.empty();
}若实现不确定,先伪造固定值没有问题;第二条 IsNotEmptyAfterPush 会使固定 true 失败。此时引入 elements_ 状态是测试要求的最小一般化,而不是提前设计完整容器。
绿灯是重构许可,不是完成仪式
↡新测试与全部回归测试都通过,且测试进程以成功状态结束的可观察状态。只有全绿才能安全区分结构变化与行为变化。重构可以改名、提取函数、消除重复、移动职责或缩小依赖,但不新增可观察需求。每次改动后立即运行相关测试,若失败,回退的认知范围只有几行代码。
template <typename T>
class Stack {
public:
bool isEmpty() const { return elements_.empty(); }
void push(T value) { elements_.push_back(std::move(value)); }
private:
std::vector<T> elements_;
};如果 push 测试通过后发现命名或重复,先重构;如果下一需求是 pop 空栈失败,结束重构并写新红灯。行为与结构交替,但不在同一个不可诊断步骤中混合。
测试清单控制焦虑与范围
↡开发开始前及过程中维护的行为例、边界和疑问列表,用于选择下一条最小红灯而非一次实现全部事项。面对新功能时,先快速列出典型行为、边界、错误与设计问题,例如:新栈为空、push 后非空、后进先出、空 pop 的契约、容量策略。清单不是提前写完测试,也不是固定计划;每完成一项就勾掉,并把新发现补入列表。
选择下一项时优先:能推进核心路径、风险高但例子小、或能迫使设计澄清的行为。不要总挑最容易的文字断言,也不要一开始选择需要十个依赖的大场景。
采用 TDD 的心态:允许不知道最终设计
↡把未知视为可由下一条失败例探索的问题,愿意以短反馈修正设计而不执着于一次预测完整结构的工作取向。有经验的开发者容易先在脑中完成所有抽象,再把测试当验证。TDD 要求保留设计判断,却延后不可逆细节:先确定最窄公开行为,让例子揭示抽象是否真的降低复杂度。删除一个不再需要的早期 helper 不是浪费,而是反馈生效。
采用初期会感觉慢,因为团队同时学习测试框架、设计分解和短步操作。应选择可控的真实功能练习,记录周期长度与卡点,而不是在第一次生产事故中才尝试整套流程。
成功的机械条件比口号更重要
↡让短反馈可持续的具体操作条件,包括一键运行、快速测试、确定数据、清晰失败、版本控制和持续重构。一套可执行机制应包括:
- 一个命令完成构建与测试,不依赖个人 IDE 状态
- 大多数单元测试在秒级完成,局部过滤和全套入口都明确
- 测试不依赖真实时间、随机顺序或共享外部状态
- 每个失败给出用例名、期望和实际,先修复首个根因
- 小提交保存可恢复点,重构与新行为尽量分开
- 持续集成执行与本地相同入口,失败不能被脚本吞掉
速度不是为了刷次数,而是让开发者愿意每几分钟运行全套。测试持续超过容忍阈值时,先定位慢依赖和分层边界,不要用减少执行频率掩盖设计问题。
所谓 mechanics for success(成功实践)不是“坚持先写测试”的口号,而是把短循环所需的命令、速度、确定性、失败诊断和恢复点固化为团队默认路径。这些机械条件降低每次执行 TDD 的摩擦,才会让正确做法在赶进度和排障时仍然可持续。
团队采用要从共同定义开始
“先写测试”如果没有共同完成定义,容易出现有人只跑局部、有人允许红灯提交、有人把集成测试当单元测试。团队至少约定:有效红灯标准、全绿范围、测试命名与失败输出、允许的替身边界、CI 入口和坏测试处理责任。
代码评审应询问行为证据:哪个测试先失败,它为什么失败,最小实现是什么,重构保持了哪些行为。不是要求展示完整操作录像,而是让变更能够说明从需求到证据的链。
何时不机械套用
探索陌生 API、性能原型或一次性迁移脚本,可能先做短暂 spike 获取知识。spike 的产物是学习,不应直接伪装成生产实现;保留结论,丢弃探索代码,再从可观察行为开始测试驱动。无法自动化的硬件交互也可以把纯逻辑和协议边界先驱动出来,把少数真实设备验证放到更外层。
TDD 不消除系统测试、性能测试或人工体验验证。它负责缩短代码设计反馈,其他层继续证明组件组合和用户价值。
第一步:用三条规则限制当前步幅
只写足以失败的测试,只写足以通过的生产代码;遇到想提前加入的功能,把它记入测试清单而不是偷偷实现。
小结
- 单元测试验证窄、快速、确定的行为边界,不机械等同于一个函数
- TDD 三条规则限制测试与生产代码步幅,使失败保持单一可诊断
- 正确红灯必须来自目标行为;环境故障和旧测试失败不能提供新需求证据
- 从红到绿可用伪实现、显而易见实现和三角测量,最小实现不等于低质量代码
- 全绿后才能重构,测试清单负责保存未来行为而不提前实现
- TDD 心态与一键运行、快速确定测试、清晰失败和 CI 等机制共同决定能否持续
练习
- 问题 1:判断红灯。 新测试无法运行,因为动态库缺失;另一个新测试因
Stack::size()未声明而编译失败。哪一个能驱动生产代码?
- 问题 2:选择变绿策略。 第一个折扣例只要求会员返回 90,算法尚不确定;第二例将要求非会员返回 100。应怎样推进?
- 问题 3:设计采用机制。 团队测试经常十分钟才跑一次,失败还会被脚本忽略。给出最先修复的三项机制。
名词解释
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 单元测试
- 在隔离可控条件下快速验证窄行为并给出确定结果的自动化检查。
- 测试驱动开发
- 让失败测试决定下一行为,再以最小实现和重构推进设计的方法。
- TDD 三条规则
- 限制何时写测试和生产代码以及每次写多少的三项纪律。
- 正确红灯
- 因目标行为缺失而按预测失败、消息可解释的测试状态。
- 最小实现
- 只满足当前失败例且不预付无证据行为的生产代码。
- 全绿
- 新测试和全部回归测试均成功且进程返回成功状态。
- 测试清单
- 用于选择下一条小行为并保存边界与疑问的动态列表。
- TDD 心态
- 允许最终设计未知并用短反馈持续修正结构的工作取向。
- TDD 成功机制
- 支撑短反馈的一键运行、速度、确定性、清晰失败与持续集成条件。