Chapter 3:Test-Driven Development Foundations

对齐原书 Chapter 3:理解单元测试和 TDD 基础、红绿重构、TDD 三条规则、从红到绿的最小实现策略,以及让个人和团队成功采用 TDD 的心态与机械习惯。

学习目标

  • 能用 TDD 三条规则约束测试与生产代码的切换,识别有效红灯、环境伪红灯和过度实现
  • 能从一个失败例通过伪实现、显而易见实现或三角测量得到最小绿灯,再在全绿下重构
  • 能建立测试清单、短运行入口和失败恢复纪律,把 TDD 从个人技巧变成可持续的团队反馈机制

机制总览

Chapter 3:Test-Driven Development Foundations:机制路径

  1. 1

    从示例上升为可重复的工作协议

    上一章展示了 Soundex 的连续小步。本章回答更一般的问题:什么算单元测试,红绿重构为何必须按顺序,三条规则如何限制步幅,卡在红灯时怎样前进,以及团队为何常在“测试太慢”“需求太急”时放弃纪律。

  2. 2

    TDD 是设计反馈,不是测试数量竞赛

    TDD 的直接产物是测试,但主要价值是更早暴露接口难用、责任混合和依赖不可控。先站在调用者角度写示例,会迫使代码在实现之前回答:输入怎样表达,结果怎样观察,失败怎样报告,协作者怎样替换。

  3. 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 三条规则各自限制一种过量变化:第一条阻止无需求证据的生产代码;第二条阻止一次写出大量测试;第三条阻止为了想象中的未来提前一般化。它们不是法律,也不禁止思考,而是让每次变化足够小,以便失败时能立即指出原因。

例如空栈行为尚不存在时,测试可以先因 Stack::isEmpty 未声明而编译失败。按第二条规则,这已经足够红;先只添加接口让测试继续执行,然后观察值断言失败。一次同时添加 pushpop、迭代器和动态扩容,会让下一批测试失去设计压力。

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 不消除系统测试、性能测试或人工体验验证。它负责缩短代码设计反馈,其他层继续证明组件组合和用户价值。

分步1 / 3

第一步:用三条规则限制当前步幅

只写足以失败的测试,只写足以通过的生产代码;遇到想提前加入的功能,把它记入测试清单而不是偷偷实现。

小结

  • 单元测试验证窄、快速、确定的行为边界,不机械等同于一个函数
  • TDD 三条规则限制测试与生产代码步幅,使失败保持单一可诊断
  • 正确红灯必须来自目标行为;环境故障和旧测试失败不能提供新需求证据
  • 从红到绿可用伪实现、显而易见实现和三角测量,最小实现不等于低质量代码
  • 全绿后才能重构,测试清单负责保存未来行为而不提前实现
  • TDD 心态与一键运行、快速确定测试、清晰失败和 CI 等机制共同决定能否持续

练习

  1. 问题 1:判断红灯。 新测试无法运行,因为动态库缺失;另一个新测试因 Stack::size() 未声明而编译失败。哪一个能驱动生产代码?
  1. 问题 2:选择变绿策略。 第一个折扣例只要求会员返回 90,算法尚不确定;第二例将要求非会员返回 100。应怎样推进?
  1. 问题 3:设计采用机制。 团队测试经常十分钟才跑一次,失败还会被脚本忽略。给出最先修复的三项机制。

名词解释

名词解释

本章出现的专业名词,用大白话再讲一遍。

单元测试
在隔离可控条件下快速验证窄行为并给出确定结果的自动化检查。
测试驱动开发
让失败测试决定下一行为,再以最小实现和重构推进设计的方法。
TDD 三条规则
限制何时写测试和生产代码以及每次写多少的三项纪律。
正确红灯
因目标行为缺失而按预测失败、消息可解释的测试状态。
最小实现
只满足当前失败例且不预付无证据行为的生产代码。
全绿
新测试和全部回归测试均成功且进程返回成功状态。
测试清单
用于选择下一条小行为并保存边界与疑问的动态列表。
TDD 心态
允许最终设计未知并用短反馈持续修正结构的工作取向。
TDD 成功机制
支撑短反馈的一键运行、速度、确定性、清晰失败与持续集成条件。

讨论

评论区加载中…