转自:http://www.jianshu.com/p/62f16cd4fef3

本文结构:

  • 什么是 TDD
  • 为什么要 TDD
  • 怎么 TDD
  • FAQ
  • 学习路径
  • 延伸阅读

什么是 TDD

TDD 有广义和狭义之分,常说的是狭义的 TDD,也就是 UTDD(Unit Test Driven Development)。广义的 TDD 是 ATDD(Acceptance Test Driven Development),包括 BDD(Behavior Driven Test 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 编码方式

传统编码方式

  • 需求分析,想不清楚细节,管他呢,先开始写
  • 发现需求细节不明确,去跟业务人员确认
  • 确认好几次终于写完所有逻辑
  • 运行起来测试一下,靠,果然不工作,调试
  • 调试好久终于工作了
  • 转测试,QA 测出 bug,debug, 打补丁
  • 终于,代码可以工作了
  • 一看代码烂的像坨屎,不敢动,动了还得手工测试,还得让 QA 测试,还得加班...

TDD 编码方式

  • 先分解任务,分离关注点(后面有演示)
  • 列 Example,用实例化需求,澄清需求细节
  • 写测试,只关注需求,程序的输入输出,不关心中间过程
  • 写实现,不考虑别的需求,用最简单的方式满足当前这个小需求即可
  • 重构,用手法消除代码里的坏味道
  • 写完,手动测试一下,基本没什么问题,有问题补个用例,修复
  • 转测试,小问题,补用例,修复
  • 代码整洁且用例齐全,信心满满地提交

TDD 的好处

降低开发者负担

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

保护网

TDD 的好处是覆盖完全的单元测试,对产品代码提供了一个保护网,让我们可以轻松地迎接需求变化改善代码的设计

所以如果你的项目需求稳定,一次性做完,后续没有任何改动的话,能享受到 TDD 的好处就比较少了。

提前澄清需求

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

快速反馈

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

怎么 TDD


TDD

TDD 的基本流程是:红,绿,重构。

更详细的流程是:

  • 写一个测试用例
  • 运行测试
  • 写刚好能让测试通过的实现
  • 运行测试
  • 识别坏味道,用手法修改代码
  • 运行测试

你可能会问,我写一个测试用例,它明显会失败,还要运行一下吗?

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

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

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

TDD 的三条规则

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

如果违反了会怎么样呢?

违反第一条,先编写了产品代码,那这段代码是为了实现什么需求呢?怎么确保它真的实现了呢?

违反第二条,写了多个失败的测试,如果测试长时间不能通过,会增加开发者的压力,另外,测试可能会被重构,这时会增加测试的修改成本。

违反第三条,产品代码实现了超出当前测试的功能,那么这部分代码就没有测试的保护,不知道是否正确,需要手工测试。可能这是不存在的需求,那就凭空增加了代码的复杂性。如果是存在的需求,那后面的测试写出来就会直接通过,破坏了 TDD 的节奏感。

我认为它的本质是:
分离关注点,一次只戴一顶帽子

在我们编程的过程中,有几个关注点:需求,实现,设计。

TDD 给了我们明确的三个步骤,每个步骤关注一个方面。

红:写一个失败的测试,它是对一个小需求的描述,只需要关心输入输出,这个时候根本不用关心如何实现。

绿:专注在用最快的方式实现当前这个小需求,不用关心其他需求,也不要管代码的质量多么惨不忍睹。

重构:既不用思考需求,也没有实现的压力,只需要找出代码中的坏味道,并用一个手法消除它,让代码变成整洁的代码。

注意力控制

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

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

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

不会合理拆分任务

TDD 之前要拆分任务,把一个大需求拆成多个小需求。

也可以拆出多个函数来。

不会写测试

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

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

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

不会写刚好的实现

很多人写实现的时候无法专注当前需求,一不小心就把其他需求也实现了,就破坏了节奏感。

实现的时候不会小步快走。

不会重构

不懂什么是 Clean Code,看不出 Smell,没有及时重构,等想要重构时已经难以下手了。

不知道用合适的「手法」消除 Smell。

基础设施

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

实例


写一个程序来计算一个文本文件 words.txt 中每个单词出现的频率。

为了保持简单,假设:

  • words.txt 只包含小写字母和空格
  • 每个单词只包含小写字母
  • 单词之间由一个或多个空格分开

举个例子,假设 words.txt 包含以下内容:

the day is sunny the the

the sunny is is

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

the 4

is 3

sunny 2

day 1


请先不要往下读,思考一下你会怎么做。

(思考 3 分钟...)

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

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

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

                            这种代码没有任何的封装。这就是为什么很多人一听到说有些公司限制一个方法不超过 10 行,就立马跳出来说,这不可能,10 行能干什么啊,我们的业务逻辑很复杂...
                            这样的代码存在什么样的问题呢?

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

                            好嘛,那我们来 TDD 嘛,你说读文件,输出控制台的测试代码要怎么写?

                            当然,我们可以通过 Mock 和 Stub 来隔离 IO,但真的有必要吗?

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

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

                            Kent Beck 说:公司请我来是为了实现业务价值,而不是写测试代码。

                            所以我只在没有信心的地方写测试代码。

                            那对我们这个程序而言,读文件和打印到控制台都是调用系统 API,可以很有信心吧。最没有信心的是中间那写要自己写的业务逻辑。

                            所以我们可以对程序做一些封装,《代码整洁之道》里说,有注释的地方都可以抽取方法,用方法名来代替注释:

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

                                          这样是不是就可以单独为 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",多个空格,完善分割单词的代码

                                          Martin Fowler 的观点是,以前我们写代码要做 Big Front Up Design,在开始写代码前要设计好所有细节。

                                          而我们有了重构这个工具后,做设计的压力小了很多,因为有测试代码保护,我们可以随时重构实现了。但这并不代表我们不需要做提前设计了,提前设计可以让我们可以和他人讨论,可以先迭代几次再开始写代码,在纸上迭代总比改代码要快。

                                          我个人比较认同 Martin Fowler 的做法,先在脑子里(当然,我脑子不够用,所以用纸画)做设计,迭代几次之后再开始写,这样,我还是会用最简单的实现通过测试,但重构时就有了方向,效率更高。

                                          回到这个程序,我发现目前的封装不在一个抽象层次上,更理想的设计是:


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

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


                                                                现在开始,只要照着 红-绿-重构 的循环去做就可以。
                                                                大部分 TDD 做不好,就是没有前面的任务分解和列 Example 的过程。

                                                                想看 TDD 过程的话,可以参考我做的直播

                                                                或者如果需要,我也可以录一个这个题目的视频。

                                                                FAQ

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

                                                                行,但是要写完实现后,马上写测试,用测试来验证实现。如果你先手工测试,把代码都调试好了,再补单元测试,你就会觉得很鸡肋,还增加了工作量。

                                                                不管测试先行还是后行都可以享受到快速反馈,不过如果测试先行,你就可以享受另一个好处,使用意图驱动编程减少返工。因为你的测试代码就是产品代码的客户端(调用者),你可以在测试代码里写成你理想的样子(方法名,参数,返回值等),再去实现产品代码,比起先写实现后写测试,前者返工更少。

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

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

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

                                                                步子迈太大,容易扯着蛋。

                                                                练习的时候需要养成小步的习惯,工作的时候可以自由切换步子的大小。

                                                                当你自信的时候步子就可以大点,当你不太自信的时候就可以立即切换到小步的模式。如果只会大步,就难以再小步了。

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

                                                                维护时也遵循 TDD 流程,先修改测试代码成需求变更后的样子,让测试失败,再修改产品代码使其通过。

                                                                这样你就不是在维护测试用例,而是在利用测试用例。

                                                                为什么要快速实现?

                                                                其实是用二分查找法隔离问题,通过 hardcode 实现通过测试后,就基本确定测试是没有问题,这时再去实现产品代码,如果测试不通过,就是产品代码的问题。

                                                                所以小步快走主要是为了隔离问题,也就是你可以告别 Debug 了。

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

                                                                如果一个测试失败了,修复的时候是改测试代码而不是产品代码,那就是测试代码写的不好。

                                                                当测试代码足够简单时,如果一个测试失败了,就有足够信心断定一定是产品代码的问题。

                                                                什么时候不适合 TDD?

                                                                如果你是做探索性的技术研究(Spike),不需要长期维护,而且测试基础设施搭建成本很高,那还是手工测试吧。

                                                                另外还有「可测试性极差的遗留系统」和「使用测试不友好的技术栈」的系统,做 TDD 可能得不偿失。

                                                                学习路径

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

                                                                延伸阅读

                                                                Q&A

                                                                问:在一个团队中谁来写单元测试呢,开发还是测试?开发测试如何协作呢?分开的方式会不会导致测试用例质量的下降?毕竟要实现的功能只有开发最清楚,会不会导致覆盖率降低?

                                                                答: TDD 是开发人员自己的事。基本不需要和测试协作,不过我司的测试已经开始参与单元测试的 Review 了。

                                                                问:TDD是否适合界面的开发?应用的业务逻辑部分可以用TDD这个可以理解,不过,界面部分是否也适用?如果可以的话,大概是如何操作呢?比如安卓,是用安卓提供的测试框架么?那只能测试简单的界面切换之类的功能,有没有其他更好的方式?

                                                                答:前几年我们做过一个 Android 应用,用 Robolectric 做的单元测试。http://robolectric.org/

                                                                移动应用开发挺适合的,因为你就不用启动应用来测试了。反馈会比较快,也不用去做数据,去跳转页面。Robolectric 不用启虚拟机,速度非常快,在 JVM 里跑的。也可以用 Appium 做端到端的测试。

                                                                问:有关于怎样分解任务的策略,拿到一个任务,如何开始第一步?

                                                                答:关于任务分解可以读这个系列文章,学会这个,就可以在不动键盘的情况下想清楚代码的结构,在脑子里做几遍重构,再写的时候就效率很高。脑子想不明白可以借助纸笔来画。有了这个图你就可以和别人交流了。带人的时候尤其好用,你布置完一个任务,让新人先画图,就可以在他开始写代码前对他的设计提反馈。

                                                                问:按流程一路写测试,一路实现,还是随便挑一个开始测试与实现,然后mock 数据呢?

                                                                答:有从上到下的,也有从下到上的两种途径。我个人倾向于从上到下的。或者说从外到内的。从下到上,有可能最后连不起来。

                                                                问:单元测试与集成测试,怎么权衡,开发应该注重单元测试还是集成测试,还是说都要关注?

                                                                答:测试是分层的,都要关注。各种测试要搭配起来,各有各的好处。

                                                                问:如何在紧张的工期和完善的单元测试之间进行权衡?

                                                                答:这是个伪命题。TDD 不是慢。

                                                                问:两周一个版本迭代,适合用TDD吗?

                                                                答:特别适合。没有完善的测试覆盖,两周发布一个版本是很困难的。有 TDD 才容易做到快速交付,写完测试,实现,跑完构建流程,部署流水线,就发布了。

                                                                问:我比较关心 Robert 大叔说的那个TDD优先级 ,这个如何理解和应用?好像确实很容易写着就死胡同了。

                                                                答:这个就是 TDD 中绿的部分,Baby Step 能做到多细。Uncle Bob 的 TPP 给我们指了一条明路。

                                                                问:TDD测试驱动开发,有哪些注意事项,测试和开发应该注意哪些内容?

                                                                答:小步快走,频繁提交,注意休息。

                                                                问:对于单元测试的mock,应该针对外部接口进行mock吗?那么,再提交测试之前,是否要关注外部接口的返回结果是否按照约定返回?

                                                                答:单元测试保证的是自己的逻辑。外部接口的质量有外部系统来保证。我们也有集成测试来保证。

                                                                问:请老师分享一下写单元测试好的实践,及各种单元测试优缺点(使用详解就更好了)。

                                                                答:三个原则,Given-When-Then,FIRST,BICEP-Right。

                                                                作者:SeabornLee

                                                                链接:http://www.jianshu.com/p/62f16cd4fef3

                                                                來源:简书

                                                                著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

                                                                深度解读 - TDD(测试驱动开发)的更多相关文章

                                                                1. TDD(测试驱动开发)培训录

                                                                  2014年我一直从事在敏捷实践咨询项目,这也是我颇有收获的一年,特别是咨询项目的每一点改变,不管是代码质量的提高,还是自组织团队的建设,都能让我们感到欣慰.涉及人的问题都是复杂问题,改变人,改变一个组 ...

                                                                2. TDD(测试驱动开发)培训录(转)

                                                                  本文转载自:http://www.cnblogs.com/whitewolf/p/4205761.html 最近也在了解TDD,发现这篇文章不错,特此转载一下. TDD(测试驱动开发)培训录 2015 ...

                                                                3. TDD(测试驱动开发)学习一:初识TDD

                                                                  首先说一下名词解释,TDD,英文名称Test-Driven Development,中文名称测试驱动开发,简单的断下句“测试/驱动/开发”,简单的理解一下,就是测试驱动着开发,大白话就是说用一边测试一 ...

                                                                4. TDD(测试驱动开发)

                                                                  TDD(测试驱动开发)培训录 2014年我一直从事在敏捷实践咨询项目,这也是我颇有收获的一年,特别是咨询项目的每一点改变,不管是代码质量的提高,还是自组织团队的建设,都能让我们感到欣慰.涉及人的问题都 ...

                                                                5. TDD(测试驱动开发)学习二:创建第一个TDD程序

                                                                  本节我们将学习一些测试驱动开发环境的搭建,测试驱动开发概念和流程.所涉及的内容全部会以截图的形式贴出来,如果你也感兴趣,可以一步一步的跟着来做,如果你有任何问题,可以进行留言,我也会很高兴的为你答疑. ...

                                                                6. (译)TDD(测试驱动开发)的5个步骤

                                                                  原文:5 steps of test-driven development https://developer.ibm.com/articles/5-steps-of-test-driven-deve ...

                                                                7. 深度解读 - TDD详细(测试驱动开发)

                                                                  本文结构: 什么是 TDD 为什么要 TDD 怎么 TDD FAQ 学习路径 延伸阅读 什么是 TDD TDD 有广义和狭义之分,常说的是狭义的 TDD,也就是 UTDD(Unit Test Driv ...

                                                                8. 基于SOA架构的TDD测试驱动开发模式

                                                                  以需求用例为基,Case&Coding两条线并行,服务(M)&消费(VC)分离,单元.接口.功能.集成四层质量管理,自动化集成.测试.交付全程支持. 3个大阶段(需求分析阶段.研发准备 ...

                                                                9. TDD(测试驱动开发)的推广方法论

                                                                随机推荐

                                                                1. 报错OPTION SQL_SELECT_LIMIT=

                                                                  org.quartz.JobPersistenceException: Couldn't acquire next trigger: You have an error in your SQL syn ...

                                                                2. 【时光回溯】【JZOJ3567】【GDKOI2014】石油储备计划

                                                                  题目描述 输入 输出 对于每组数据,输出一个整数,表示达到"平衡"状态所需的最小代价. 样例输入 2 3 6 1 5 1 2 1 2 3 2 5 4 5 4 3 2 1 3 1 1 ...

                                                                3. ajax嵌套陷阱

                                                                  ajax嵌套陷阱 $('.ajaxupd').click(function () { $('.shadow1').show(); $('.update').show(); var tds=$(this ...

                                                                4. Java8中的LocalDateTime工具类

                                                                  网上搜索了半天都没有找到Java8的LocalDateTime的工具类,只好自己写了一个,常用功能基本都有.还在用Date的Java同道该换换了. 个人项目地址:https://github.com/ ...

                                                                5. day39-Spring 06-Spring的AOP:带有切点的切面

                                                                  环绕增强功能是最强的,它相当于前置增强和后置增强. 这就是带有切点的切面 package cn.itcast.spring3.demo4; import org.aopalliance.interce ...

                                                                6. 直击 KubeCon 2019 现场,阿里云 Hands-on Workshop 亮点回顾

                                                                  2019 年 6 月 24 日,KubeCon + CloudNativeCon 第二次在中国举办.此次大会阿里共有 26 个技术演讲入选,并有两场沙龙活动,阿里云专家也与技术极客们也再次相聚.Kub ...

                                                                7. 【转载】Combination Sum

                                                                  Combination Sum Given a set of candidate numbers (C) and a target number (T), find all unique combin ...

                                                                8. Mybatis通过工具类根据用户名查找用户列表

                                                                  抽取SqlSessionFactoryUtils工具类,共享SqlSessionFactory创建过程 /** * SqlSessionFactory工具类 * @author:Mr.Tan * @C ...

                                                                9. 14 个你可能不知道的 JavaScript 调试技巧

                                                                  了解你的工具可以极大的帮助你完成任务.尽管 JavaScript 的调试非常麻烦,但在掌握了技巧 (tricks) 的情况下,你依然可以用尽量少的的时间解决这些错误 (errors) 和问题 (bug ...

                                                                10. @loj - 2093@ 「ZJOI2016」线段树

                                                                  目录 @description@ @solution@ @accepted code@ @details@ @description@ 小 Yuuka 遇到了一个题目:有一个序列 a1,a2,..., ...