【TDD】深度解读 – TDD(测试驱动开发)

【TDD】深度解读 – TDD(测试驱动开发)TDD 是一种在开发功能代码前先编写测试用例的开发方法 旨在降低开发者负担 提供保护网 提前澄清需求并实现快速反馈

大家好,欢迎来到IT知识分享网。

【TDD】深度解读 – TDD(测试驱动开发)

什么是 TDD

TDD 有广义和狭义之分,常说的是狭义的 TDD,也就是 UTDD(Unit Test Driven Development)。广义的 TDD 是 ATDDAcceptance Test Driven Development),包括 BDDBehavior Driven Development)和 Consumer-Driven Contracts Development 等。
本文所说的 TDD 指狭义上的 TDD,也就是「单元测试驱动开发」。

TDD敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码TDD 是 XP(Extreme Programming)的核心实践。它的主要推动者是 Kent Beck

TDD 有三层含义

  • Test-Driven Development测试驱动开发
  • Task-Driven Development任务驱动开发,要对问题进行分析并进行任务分解。
  • Test-Driven Design测试保护下的设计改善。TDD 并不能直接提高设计能力,它只是给你更多机会和保障去改善设计。

为什么要 TDD

传统编码方式 VS TDD 编码方式

传统编码方式
  1. 需求分析,想不清楚细节,管他呢,先开始写
  2. 发现需求细节不明确,去跟业务人员确认
  3. 确认好几次终于写完所有逻辑
  4. 运行起来测试一下,靠,果然不工作,调试
  5. 调试好久终于工作了
  6. 转测试,QA 测出 bug,debug, 打补丁
  7. 终于,代码可以工作了
  8. 一看代码烂的像坨屎,不敢动,动了还得手工测试,还得让 QA 测试,还得加班…
TDD 编码方式
  1. 先分解任务,分离关注点(后面有演示)
  2. 列 Example,用实例化需求,澄清需求细节
  3. 写测试,只关注需求,程序的输入输出,不关心中间过程
  4. 写实现,不考虑别的需求,用最简单的方式满足当前这个小需求即可
  5. 重构,用手法消除代码里的坏味道
  6. 写完,手动测试一下,基本没什么问题,有问题补个用例,修复
  7. 转测试,小问题,补用例,修复
  8. 代码整洁且用例齐全,信心满满地提交

TDD 的好处

降低开发者负担

通过明确的流程,让我们一次只关注一个点,思维负担更小。

保护网

TDD 的好处是覆盖完全的单元测试,对产品代码提供了一个保护网,让我们可以轻松地迎接需求变化改善代码的设计
所以如果你的项目需求稳定,一次性做完,后续没有任何改动的话,能享受到 TDD 的好处就比较少了。

提前澄清需求

先写测试可以帮助我们去思考需求,并提前澄清需求细节,而不是代码写到一半才发现不明确的需求。

快速反馈

有很多人说 TDD 时,我的代码量增加了,所以开发效率降低了。但是,如果没有单元测试,你就要手工测试,你要花很多时间去准备数据,启动应用,跳转界面等,反馈是很慢的。准确说,快速反馈是单元测试的好处。

怎么 TDD

TDD 的基本流程是:红,绿,重构
更详细的流程是:

  1. 写一个测试用例
  2. 运行测试
  3. 写刚好能让测试通过的实现
  4. 运行测试
  5. 识别坏味道,用手法修改代码
  6. 运行测试
    你可能会问,我写一个测试用例,它明显会失败,还要运行一下吗?

是的。你可能以为测试只有成功和失败两种情况,然而,失败有无数多种,运行测试才能保证当前的失败是你期望的失败。

一切都是为了让程序符合预期,这样当出现错误的时候,就能很快定位到错误(它一定是刚刚修改的代码引起的,因为一分钟前代码还是符合我的预期的)。

通过这种方式,节省了大量的调试代码的时间。

TDD 的三条规则

  1. 除非是为了使一个失败的 unit test 通过,否则不允许编写任何产品代码
  2. 在一个单元测试中,只允许编写刚好能够导致失败的内容(编译错误也算失败)
  3. 只允许编写刚好能够使一个失败的 unit test 通过的产品代码

如果违反了会怎么样呢?

  1. 违反第一条,先编写了产品代码,那这段代码是为了实现什么需求呢?怎么确保它真的实现了呢?
  2. 违反第二条,写了多个失败的测试,如果测试长时间不能通过,会增加开发者的压力,另外,测试可能会被重构,这时会增加测试的修改成本。
  3. 违反第三条,产品代码实现了超出当前测试的功能,那么这部分代码就没有测试的保护,不知道是否正确,需要手工测试。可能这是不存在的需求,那就凭空增加了代码的复杂性。如果是存在的需求,那后面的测试写出来就会直接通过,破坏了 TDD 的节奏感。

:写一个失败的测试,它是对一个小需求的描述,只需要关心输入输出,这个时候根本不用关心如何实现。
绿:专注在用最快的方式实现当前这个小需求,不用关心其他需求,也不要管代码的质量多么惨不忍睹。
重构:既不用思考需求,也没有实现的压力,只需要找出代码中的坏味道,并用一个手法消除它,让代码变成整洁的代码。

注意力控制

人的注意力既可以主动控制,也会被被动吸引。注意力来回切换的话,就会消耗更多精力,思考也会不那么完整。

使用 TDD 开发,我们要主动去控制注意力,写测试的时候,发现一个类没有定义,IDE 提示编译错误,这时候你如果去创建这个类,你的注意力就不在需求上了,已经切换到了实现上,我们应该专注地写完这个测试,思考它是否表达了需求,确定无误后再开始去消除编译错误。

为什么很多人做 TDD 都做不起来?

不会合理拆分任务

不会写测试

什么是有效的单元测试,有很多人写测试,连到底在测什么都不清楚,也可能连断言都没有,通过控制台输出,肉眼对比来验证。

好的单元测试应该符合几条原则:

  • 简单,只测试一个需求
  • 符合 Given-When-Then 格式
  • 速度快
  • 包含断言
  • 可以重复执行

不会写刚好的实现

不会重构

不懂什么是 Clean Code,看不出 Smell,没有及时重构,等想要重构时已经难以下手了。
不知道用合适的「手法」消除 Smell。

基础设施

对于特定技术栈,没有把单元测试基础设施搭建好,导致写测试时无法专注在测试用例上。

实例

  • words.txt 只包含小写字母和空格
  • 每个单词只包含小写字母
  • 单词之间由一个或多个空格分开
    举个例子,假设 words.txt 包含以下内容:

你的程序应当输出如下,按频率倒序排序:

新手拿到这样的需求呢,就会把所有代码写到一个 main() 方法里,伪代码如下:

main() { 
    // 读取文件 ... // 分隔单词 ... // 分组 ... // 倒序排序 ... // 拼接字符串 ... // 打印 ... } 

思路很清晰,但往往一口气写完,最后运行起来,输出却不符合预期,然后就开始打断点调试。

  • 不可测试
  • 不可重用
  • 难以定位问题

有人问过 Kent Beck 这样一个问题:

你真的什么都会测吗?连 getter 和 setter 也会测试吗?

main() { 
    String words = read_file('words.txt') String[] wordArray = split(words) Map<String, Integer> frequency = group(wordArray) sort(frequency) String output = format(frequency) print(output) } 

这样是不是就可以单独为 splitgroupsortformat 这些方法写单元测试了呢?
当然可以, 它们的输入和输出都是很明确的嘛。

等等,你可能会说,不是测试驱动设计吗?你怎么开始做设计了?好问题!

TDD 要不要做提前设计呢?

Kent Beck 不做提前设计,他会选一个最简单的用例,直接开写,用最简单的代码通过测试。逐渐增加测试,让代码变复杂,用重构来驱动出设计。
在这个需求里,最简单的场景是什么呢?
那就是文件内容为空,输出也为空。

  • “” => “”
  • “he” => “he 1”,一个单词,驱动出格式化字符串的代码
  • “he is” => “he 1\r\nis 1”,两个不同单词,驱动出分割单词的代码
  • “he he is” => “he 2\r\nis 1”,有相同单词,驱动出分组代码
  • “he is is” => “is 2\r\nhe 1”,驱动出分组后的排序代码
  • “he is” => “he 1\r\nis 1”,多个空格,完善分割单词的代码

main() { 
     String words = read_file('words.txt') String output = word_frequency(words) print(output) } word_frequency(words) { 
     String[] wordArray = split(words) Map<String, Integer> frequency = group(wordArray) sort(frequency) return format(frequency) } 

这时候,又有两种选择,有人喜欢自顶向下,有人喜欢自底向上,我个人更倾向于前者。


FAQ

为什么一定要先写测试,后补测试行不行?

刚写了一个测试,还没写实现。明知道运行测试一定会报错,为什么还要去运行?

其实测试的运行结果并非只有通过与不通过两种,因为不通过时有很多种可能。所以在明知道一定失败的情况下去运行测试,目的是看看是不是报了期望的那个错误。

小步快走确实好,但真的需要这么小步吗?

测试代码是否会成为维护的负担?

为什么要快速实现?

为什么测试代码要很简单?

什么时候不适合 TDD?

学习路径

  1. 《有效的单元测试》
  2. 《代码整洁之道》
  3. 《重构》
  4. Transformation Priority Premise
  5. 《Test-Driven Development by Example》
  6. 《Growing Object-Oriented Software, Guided by Tests》

免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/134006.html

(0)
上一篇 2025-07-15 21:10
下一篇 2025-07-15 21:20

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注微信