TDD 的全称是 Test-Driven Development,直译为「测试驱动开发」。 另一个翻译是 Test-Driven Design 「软件驱动设计」,原因是编写测试友好的代码需要有良好的设计作为基础。
实实在在的好处
- Bug 发现的越早,修复成本越低
- 与 AI 结对编程
TDD 开发流程
API 构思与组件划分/测试先行(Test-First Development)
- 首先我们需要考虑,别人将以何种方式使用这段代码,也就是这段代码的整体对外接口部分。
- 我们可以通过写测试的方式,来感受 API 的友好程度
要求在编写任何功能代码之前,必须先编写测试用例,其深远意义在于:
- 驱动设计 迫使开发者从调用者和使用者的角度(外部视角)思考接口设计和模块划分,而不仅仅是实现细节(内部视角),这有助于催生低耦合、高内聚的代码。
- 明确需求 测试用例充当了活的、可执行的需求规格说明(Specification by Example)。它们通过具体的例子来澄清需求,减少开发者和产品负责人之间的认知偏差。
- 保证可测试性 由于代码生来就是为了通过测试,因此天然就具备了良好的可测试性。
在确定了 API 的形式之后,我们需要大致构思如何实现这个功能
功能分解与任务列表 Todo List ⭐️⭐️⭐️⭐️⭐️(TDD 核心之一)
- 在 API 与实现方式有了方向之后,我们就可以根据需求的描述对功能进行分解了。
- 这里可以先不求全面,有个大致的范围即可
- 三个路径
- happy path:正常功能
- sad path:异常判断
- default path:默认值
“红-绿-重构” 循环(The Three Laws of TDD)
先让我们选择最简单的任务,并通过红绿循环实现它
- 红(Red) 先编写一个失败的测试用例,定义期望的功能或接口。运行测试看到失败(红色)是第一步,这确认了测试是有效的,并且功能尚不存在。
- 绿(Green) 编写最少且刚好能通过测试的功能代码。此阶段不求代码完美,只求快速让测试通过(绿色),验证功能基本正确。
- 重构(Refactor) 在测试通过的保护下,优化代码结构,消除重复,提升可读性和可维护性,同时确保所有测试始终保持通过。
测试隔离(Test Isolation)
不同代码的测试应该相互隔离,对一块代码的测试只考虑此代码的测试,不要考虑其实现细节。 这通常通过Mocking和Stubbing等测试替身(Test Doubles)技术来实现,用于隔离被测单元与其依赖项,确保测试的专注性、独立性和运行速度。
小结
实践 TDD 最终是为了更快地交付更多价值,其目标是通过编写测试来驱动开发,提升代码质量,简化设计,并促进团队协作。
- 测试先行和测试列表明确了要做什么;
- 红-绿-重构循环规定了具体怎么做;
- 测试隔离保证了测试的独立性与可靠性;
- 即时/持续重构则在此过程中不断优化设计。
示例
FizzBuzz (嘶嘶声嗡嗡声)
这是一个在程序员面试中很常见的编程题,也非常适合用来讲解 TDD。
问题:实现一个函数,接受一个正整数 n,返回一个列表或数组。
- 如果数字能被 3 整除,用 “Fizz” 替代。
- 如果能被 5 整除,用 “Buzz” 替代。
- 如果能同时被 3 和 5 整除,用 “FizzBuzz” 替代。
- 否则,返回数字本身。
视频演示
时长约 30 分钟
视频中的练习源自 Robert C. Martin 的 Clean Code 第十四章的一个例子。需求描述如下:
我们中的大多数人都不得不时不时地解析一下命令行参数。如果我们没有一个方便的工具,那么我们就简单地处理一下传入 main 函数的字符串数组。有很多开源工具可以完成这个任务,但它们可能并不能完全满足我们的要求。所以我们再写一个吧。
传递给程序的参数由标志和值组成。标志应该是一个字符,前面有一个减号。每个标志都应该有零个或多个与之相关的值。例如:
-l -p 8080 -d /usr/logs
“l”(日志)没有相关的值,它是一个布尔标志,如果存在则为 true,不存在则为 false。“p”(端口)有一个整数值,“d”(目录)有一个字符串值。标志后面如果存在多个值,则该标志表示一个列表:
-g this is a list -d 1 2 -3 5
“g"表示一个字符串列表[“this”, “is”, “a”, “list”],“d"标志表示一个整数列表[1, 2, -3, 5]。
如果参数中没有指定某个标志,那么解析器应该指定一个默认值。例如,false 代表布尔值,0 代表数字,”“代表字符串,[]代表列表。如果给出的参数与模式不匹配,重要的是给出一个好的错误信息,准确地解释什么是错误的。
确保你的代码是可扩展的,即如何增加新的数值类型是直接和明显的。
评审代码
通过小步快跑,多次迭代后将得到:代码即文档,测试即文档
不要过于追求测试覆盖率
目的是尽早发现问题,保证核心逻辑正确。过度追求覆盖率容易偏离工作重心。