TDD 测试驱动开发入门分享

TDD 学习笔记 | 极客时间 | 徐昊·TDD 项目实战 70 讲

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” 替代。
  • 否则,返回数字本身。

视频演示

链接地址 01|TDD演示(1):任务分解法与整体工作流程

时长约 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 代表数字,”“代表字符串,[]代表列表。如果给出的参数与模式不匹配,重要的是给出一个好的错误信息,准确地解释什么是错误的。

确保你的代码是可扩展的,即如何增加新的数值类型是直接和明显的。

评审代码

通过小步快跑,多次迭代后将得到:代码即文档,测试即文档

不要过于追求测试覆盖率

目的是尽早发现问题,保证核心逻辑正确。过度追求覆盖率容易偏离工作重心。

学习资源

comments powered by Disqus