开篇词 为什么TDD是当前最具工程效能的研发流程?
你好,我是徐昊,欢迎和我一起学习测试驱动开发(Test-Driven Development,TDD)。
对于测试驱动开发,稍有了解而全无实践的人,会认为是天方夜谭,甚至无法想象为什么需要这样的方式来开发:
- 为什么要开发人员来写测试?这难道不是测试人员的工作吗?难道开发写了测试,测试人员就不用再测了嘛?
- 又要写测试,又要写生产代码,效率是不是太低了?只写生产代码效率应该更高吧?
- 不写测试我也能写出可以工作的软件,那么写测试能给我带来什么额外的好处呢?
的确,从直觉上来看,测试驱动开发相当令人困惑:它将我们通常认为的辅助性工作——测试,作为程序员编码的主要驱动力;它主张通过构造一系列自动化测试(由程序员编写),为编写生产代码(Production Code)做指引;它甚至建议,如果不存在失败的测试,就不要编写生产代码。
看起来,似乎测试驱动开发有些过分强调测试对于程序员的重要性了。
那么我们就需要仔细思考,“测试”在所谓的“正常软件开发模式”中,到底发挥着怎样的作用。当明晰了测试驱动开发的这个核心逻辑之后,我们才能讨论是不是过分强调了。
隐式程序员测试(Implicit Developer Testing)
直觉和经验告诉我们,在所谓的“正常软件开发模式”中,貌似测试只是最后的验收步骤,程序员很少直接参与。但事实却不是这样,就算是所谓的“正常软件开发模式”,也蕴含着非常多“程序员测试”的步骤。只不过这些“程序员测试”并不表现为自动化测试,而是由“测试应用”、“跑一下”和“调试”等隐含手段体现的。
“测试应用”(Testing Application)
所谓“测试应用”并不是某个技巧正式的名字,但是所有人都熟知这一技巧:
- 构造一个简单的控制台应用(Console Application),并提供main入口函数(Entry point);
- 在main函数中,调用所编写的代码,并通过与控制台的交互(各种println、writeline之类的函数),将结果输出在控制台上;
- 再通过观察控制台的输出,判断结果正确与否。
让我们看一个具体的例子。假设我们需要将某个对象存储到数据库中,以Java中的JPA(Jakarta Persistence API)为例,那么我们大概可以构造出这样的“测试应用”:
如视频中展示的,这个测试应用符合我们对于验证测试的一切认知:有需要被测试的行为,有明确的执行结果,以及针对结果的验证。那么我们实际上可以很容易地将它改写为自动化测试:
对比这两种做法,从意图上,我们可以粗略地认为,它们是对于同一种意图的两种不同实现:无计划的手动验证与有计划的自动化验证。所以如果你曾经使用过“测试应用”,那么你就曾经在项目中做过“程序员测试”。
“跑一下”(Run it in a local testing environment)
同样,“跑一下”也不是某个技巧的正式名字。从严谨的角度出发,“跑一下”甚至不能算是它真正的名字。它真正的名字应该叫“在我本地的测试环境中跑一下”。同样,所有人也都熟知这一技巧,就真的是“在我本地的测试环境中跑一下”。因为当代应用通常都在受控环境中运行(Managed Environment),所以当验证某个功能时,需要连通其所在的受控环境一起执行。
让我们再看一个具体的例子。假设我们需要实现REST API,以Java中的JAX-RS(Jakarta Restful WebService)为例,那么我们大概会这样来跑一下:
如视频中展示的,无论是通过浏览器直接观测结果,还是通过Postman验证,都符合我们对于验证测试的一切认知:有需要被测试的行为,有明确的执行结果,以及针对结果的验证。那么我们实际上可以很容易地将它改写为自动化测试:
对比这两种做法,从意图上,我们可以粗略地认为,它们是对于同一种意图的两种不同实现:无计划的手动验证与有计划的自动化验证。所以如果你曾经也类似这样“跑过一下”你的应用,那么你就曾经在项目中做过“程序员测试”。
“调试”(Debug)
我想你已经发现了模式,你肯定要猜测“调试”也是一种验证测试,但并不是这样!“测试应用”和“跑一下”这两种技巧更多地关注在发现问题上,可以看作是“验证测试”。而“调试”通常发生在已经明显知道有错误的代码中,是一个定位错误的过程。让我们来看个例子:
如视频中展示的,“调试”是一种启发式过程(Heuristic Procedure),更像是探索测试(Exploratory Testing),根据出现的错误寻找可能出现错误的位置,然后设置断点,判断该断点处的状态是否正确。
除了调试之外,我们还可以将代码划分成更小的单元,逐一排查以定位错误。那么我们就可以将对于某段代码的调试过程,转化成对于一组更小粒度单元的验证测试:
在软件开发中,一直都存在验证性测试和定位性测试两种测试。这也很好理解,我们既要知道代码有没有错误,还要知道当错误发生时,错误发生在哪里。
从定位性测试的角度出发,对比这两种做法,从意图上,我们可以粗略地认为,它们是对于同一种意图的两种不同实现:手动的启发式定位与有计划的逐模块自动化排查。所以如果你曾经也类似这样“调试”过你的应用,那么你就曾经在项目中做过“程序员定位测试”。
测试驱动开发的核心逻辑
除去我们讨论的三种,在所谓的“正常软件开发模式”中,还存在很多其他常用的手段,也都可以看作是自发性的“程序员测试”。任何有过严肃编程经验的从业者,都能根据自己过往的经历,回想起这些年所做过的“程序员测试”。
我们构造软件的过程,就是通过一系列验证测试(测试应用、跑一下等),证明我们在朝着正确的方向前进;如果验证的过程中发现出了错误,那么再通过一系列定位测试(调试等),找到问题的根源,加以改进。
如此往复,直到完成全部功能(如下图所示)。
现在让我们回到最初的问题:测试驱动开发过分强调测试对于程序员的重要性了吗?答案是:并没有!
验证测试与定位测试,本身就贯穿了整个软件构造的过程。测试构成了整个开发流程的骨架,功能开发可以看作填充在测试与测试之间的血肉。这就是测试驱动开发的核心逻辑:以测试作为切入点,可以提纲挈领地帮助我们把握整个研发的过程。
一个个测试就像一个个里程碑点(Milestone),规划着研发活动的进度。围绕这些里程碑点,我们就可以持续对成本和效率进行管理和改进。也就是说,测试驱动开发将个体的开发活动,变成了工程化的研发过程。这也是为什么,三十年以来,测试驱动开发在敏捷方法族中,都扮演着工程实践基石的角色。
因为测试是如此重要,我们需要非常高效地实现它们。那么“无计划的手动验证”与“手动的启发式定位”都是无法容忍的低效手段,必须将它们替换为“有计划的自动化验证测试”和“有计划的逐模块自动化排查”。从而才有了我们熟知的测试驱动开发(红/绿/重构循环),以及令没有做过测试驱动开发的人费解的对于自动化的偏执。
到这里,我想你应该就会明白了,测试驱动开发并不是关于“怎么写测试”、“怎么消除测试人员”、“怎么让开发人员多干一份活”的编码技巧。它是以测试为里程碑点的工程化研发过程;同时,测试驱动开发将软件流程中无时无处不在的低效测试手段,以可重复的、高效的自动化测试代替,以获得更高的工程效能。
这就是隐藏在测试驱动开发反直觉的工程实践背后的核心逻辑。
为什么要学习测试驱动开发?
测试驱动开发最直接的收益,就是可以提高开发者的工程效能。
工程效能不仅仅是开发功能的效能,还包含发现问题、定位问题以及修复问题的效能。从理论上来说,后面三个并不是根本的复杂性问题,但在实际中却大量存在,甚至占据一半以上的有效工作时间。因而高效地完成这些非根本性问题,就可以显著地提高效率。
其中发现错误,并准确定位错误,通过发现问题的测试和定位问题的测试可以高效实现。而如果说发现问题的测试,还有后置或外包于他人的可能,那么定位问题的测试,无论如何都没有办法了。所以实际上高效能的研发过程,至少需要我们提供可工作的代码,以及一组可用于定位问题的测试。
从这个角度出发,那么测试驱动开发仍是时至今日最具有工程效能的研发流程,没有之一。
学习测试驱动开发的难点在哪里?
学习测试驱动开发是困难的,很多信服于测试驱动开发理念而自发实践的人也会被各种问题困扰:
- 测试从哪里来?为什么我写了很多测试,功能却没有进展?
- 写什么样的测试既能驱动功能进展,又不会在重构中被破坏?
- 社区里很多人都非常推崇单元测试,但我就是要测一段SQL,单元测试怎么测?
测试驱动开发从来都不是一种即插即用的技能,它是一种工作习惯和思维方式,背后还对深层的胜任力(Competency)——分析性思考有极高的要求。某种程度上讲,测试驱动开发有点像物理,定理写出来很简单,但需要我们在不同的场景下练习,才能应用得得心应手。
正是因为这样的特点,我们的课程以视频展示为主,文字讲解为辅。希望你能在具体的场景下,体会使用TDD和平时开发的差异。具体而言,我们的课程是这样设计的。
首先我们将从一个编码练习(Code Kata)级别的小例子入手,展示使用TDD开发的全过程。因为大多数人,对于TDD没有一个感性、直观的认识。因而在任何讲解之前,我需要让你亲眼看一看,如何通过TDD的方法实现一个非常简单的功能。
然后我们会围绕这个例子,详细讲解TDD的核心理念与方法。我们将深入讨论TDD中的测试到底是什么样的测试,TDD是如何驱动我们的开发。我们会介绍TDD的经典学派(芝加哥学派)与伦敦学派不同的切入点。
在这部分的最后,我将会总结TDD作为工程方法的核心优势在什么地方。如果你需要说服周围的同事、朋友、领导开始采用TDD方法,这将给你提供足够的弹药。
然后我们将进入实战项目环节。我将以几个技术框架为例(IoC容器、RESTful框架、DB Mapper框架等),展示如何使用TDD的方式从头来实现它们,TDD实战的细节将一览无遗。
开篇寄语
作为中国最早一批测试驱动开发的实践者,从2003年开始,我就将测试驱动开发作为主要工作方式了。
在加入Thoughtworks之后,对内对外我讲述了大量测试驱动开发的课程。曾经有一段时间,每一位新入职的Thoughtworker我都会通过6周的时间,教会他们进行测试驱动开发。
当我主持Thoughtworks委培生计划——小巨人项目(Small Giant Program)时。测试驱动开发与学习管理,是最早也是最重要的工作习惯。近年我研发的高效能工程方法SEELE(Scalable Engineering Experience for Large Enterprise)也是将测试驱动开发作为核心流程,从而简化知识传递成本并提高杠杆率。
测试驱动开发伴随了我职业生涯的每一个阶段。我相信,我掌握了测试驱动开发那天,我才成为了可靠、高效的职业程序员。如果你对程序员这个职业抱有严肃的态度,那么测试驱动开发是必须要掌握的。
最后,我也很想听听你对TDD的看法,以及实践TDD过程中的体会。欢迎你分享在留言区,我们下节课再见!
- 🐑 👍(1) 💬(0)
TDD专栏福利大合集: 1、打卡赢好礼(4月23日-5月10日):正在进行中,学习专栏第1-10讲并在留言区打卡,结束后奖励; 2、代码亲手评(5月底):预计打卡结束后启动,完成前10讲的打卡,即可提交代码练习作业,徐昊老师会亲自点评; 3、线上带你练:根据专栏更新节奏和老师时间安排确定,徐昊老师会线上带四个同学手把手地改代码,敬请期待! 具体活动介绍见 👉 http://gk.link/a/11jPi
2022-04-28 - 俊伟 👍(19) 💬(1)
之前拜读《测试驱动开发》觉得这种方法真了不起,之后在工作中也是一直坚持实践。实践TDD真的可以让开发效率变快。现在写代码,早就把Postman之类的工具抛到脑后了,完全通过单元测试来构建可靠的应用。个人感觉使用TDD是一个先慢后快的过程,开头可能会慢一点,但是越往后测试越多,代码写的越快,真的很神奇。
2022-03-17 - 术子米德 👍(7) 💬(1)
🤔☕️🤔☕️🤔 * TDD带给我的最大改变,那就是先去设定验收的形式和手段,即确定验收的边界,然后整个实现都在趋近这个边界。这种方法刚开始用,很难受,原因在于自己经验不足,不知道怎么验收自己的成果。随着经验的增加,知道如何准确验收自己的成果,就会自然而然TDD。如果是分派任务给大家,个人实践下来,TDD也是优质武器,就是在分配前,对将来交付的内容有验收的预期,会极大提高交付的质量,更不会在交付时带来惊吓级别的惊喜。 * 所以说,TDD在开发初级,会有难受,在开发中级,会自然形成。如果某个所谓的资深,还在说TDD的优劣势,尤其各种不对付,那么大概率他没有体验过一把交付无缺陷的代码,更大概率他卖下过巨量技术债务,要么他本人还在还债,要么已经把债务转嫁给某些无辜开发。
2022-03-27 - CV 👍(5) 💬(1)
对于tdd的理念非常认可,但实际工作中接触的很多都是老项目,一个方法几百行起步,各种反射、静态方法,想写测试感觉都无从下手,更别说tdd落地。希望老师可以讲讲老项目如何开展测试工作
2022-03-20 - tdd学徒 👍(2) 💬(2)
很硬核 还配有字幕 感觉可以不用录音 文字加视频就好了
2022-03-17 - 钱忠飞 👍(2) 💬(1)
请问课程中有springboot下tdd落地的内容吗?
2022-03-17 - Sarah 👍(2) 💬(1)
内容非常的不错,在工作中也一直在实践,并尝试跟测试用例结合,以达到减少手动测试的目的。 对于这个学习形式,个人认为音频加视频的形式,有点打断学习的体验了,感觉可以都用视频的形式就好,不然要一直音频和视频之间切换,体验感不佳
2022-03-16 - 爱乐之城 👍(1) 💬(1)
老师讲的这三类场景日常前端开发中也会碰到,甚至更频繁,比如页面的展示就可以算是「跑一下」验证。请问老师对于前端该写哪些范围的测试,有建议或者单独的章节来讲吗?
2022-03-18 - Geek_fa4b49 👍(1) 💬(2)
非常高兴徐老师能给大家一个如此基础又如此重要的课程,几乎所有公司都在强调单元测试的重要性,但是我看过很多代码,基本都没有单元测试,好一点的团队是刚开始有单元测试,后面就没有继续维护了。大家都这么重视单元测试,但是为什么都没有坚持下来呢?主要是单元测试成为了项目开发的累赘,没有用TDD。如果是先写代码,再写单元测试,很容易给人一种多余的感受,而且为了测试而测试,就会导致测试无群无尽,甚至连整数的边界都纠结要不要一块测试了。如果用TDD,从test case的角度去设计测试,就不会存在这样的问题。 文章的一个观点非常重要,我们并不是写测试想取代测试人员,而是我们换了一种高效的测试方法而已。我们让测试粒度更细,测试自动化,提高测试的效率。无论如何程序员自己的白盒测试都是要做的,那我们使用TDD这种好的工程实践方法,可以使我们变成高效可靠的程序员。
2022-03-17 - 临风 👍(0) 💬(2)
工作快一年了,刚开始就知道了TDD,但是一直没有得其要领。后面也接触到了alpha测试,有了自己对开发测试的思考,但始终没有找到一个最佳实践,而自己对测试的理解也在一次次的开发中不断的变化。一开始初生牛犊不怕虎,看到质量较差的代码就想重构、重写,到后面逐渐不敢去碰了,保证自己的代码质量就行了。看着那些老代码,有种心有余而力不足的感觉。自己开发的代码一般都会有测试代码的覆盖,但并不是按照TDD的流程,交付时间又紧张,如果不写测试用例,就能更早是转测代码,有时也会怀疑自己写这些测试用例真的有意义吗,是不是只是自己的自我感动罢了?这次课程,感觉是打了瞌睡来了枕头,非常感谢老师和极客时间这个平台,希望未来自己能真正用好TDD,提高代码质量。
2022-03-20 - 马文龙 👍(0) 💬(1)
案例代码有gitee的链接地址吗 徐昊老师
2022-03-17 - aoe 👍(43) 💬(7)
一、 告诉大家怎么2倍速观看视频(希望这个功能能得到优化) 鼠标点击右键->显示所有控件->竖着的三个点->播放速度->滑倒底部->2 二、 视频很精彩,非常高效的教学方式,本篇读后感: 1. 当有 private 方法不方便测试时,可以直接提取到一个新的类中变成 public 方法进行测试;同时也说明当前类违反了单一原则,可以及时重构 2. 我现在正发愁 SQL 怎么测!看来能在专栏找到答案了 三、 分享自己的 TDD 学习经历 1. 一次因为把赋值为 0 写成了 1 的 Bug,差点导致公司巨额损失,吓坏了,为了避免此类错误再次发生,最终的解决方案是 TDD 2. 凑巧学到了郑烨老师的专栏《软件设计之美》,带我看到了 TDD 如何入门:从单一原则则到各种原则可以推导出各种设计模式,符合单一原则的代码就是对测试友好的 3. 接着学习了郑烨老师的其他三个专栏《程序员的测试课》、《10X程序员工作法》、《代码之丑》。 知道了一个方法如果行数太多很大原因是违反了单一原则: 10行:郑烨老师的标准 7行:《深入理解 Java 虚拟机》作者周志明老师的标准 4. 从郑烨老师的专栏中知道了两本神奇的书:《代码整洁之道》、《修改代码的艺术》 5. 阅读完《代码整洁之道》后发现还是无从进行 TDD 实践 6. 上网搜了一些国内外热心网友分享的 TDD 视频,开始模仿,虽然不专业,但总算实践了 7. 搜了一下 TDD 的书籍,买了两本:《Java 测试驱动开发》、《测试驱动开发的艺术》 8. 阅读完两本测试图书后知道了 Mockito,写起单元测试方便多了(渐渐理解代码需要对单元测试友好:单一原则、高内聚) 9. 《代码整洁之道》的作者 Bob 大叔说:重构最好的时机就是现在,但需要有单元测试做保障,工作中我也可以及时重构了 10. 正在阅读《修改代码的艺术》,期中 2.1 什么是单元测试中提到: 单元测试运行很快。如果运行缓慢,那就不是单元测试。 其他类型的测试经常会伪装成单元测试。如果出现以下情况,那么就不是单元测试: a. 测试会访问数据库 b. 测试会通过网络通信 c. 测试会访问系统文件 d. 你需要做特定的工作配置环境来运行测试(例如编辑配置文件) 11. 知道了以前启动 SpringBoot 进行的测试是集成测试而不是单元测试,因为太慢了 12. 现在不知道数据库操作、HTTP请求、void 方法怎么进行单元测试,迷茫中 13. 徐昊老师、郑烨老师、Bob 大叔都在 Thoughtworks 工作过,本专栏让我久久不能平静
2022-03-18 - danny 👍(10) 💬(0)
原来CTO真的会写代码。哈哈 一提到开发质量就会想到要UT,TDD,CodeReview。谁都知道,但落地的聊聊无几。 综观有几方面的因素: 上层只看结果,不关心过程,要的是马上上线,但出了问题,又会马后炮,为什么不UT,不Code Review。 下层想搞,但感觉浪费时间,业务代码还来不及写。但出了问题又反思应该有更好的工作方式,可看看TDD又违背行为习惯,被业务催促下,又回到老路了 也只有像老师这种大师级别的出来做个示范,让中国程序员个个成为小巨人~
2022-03-18 - 文经 👍(6) 💬(0)
在直播中,老师提到Beck Kent的《测试驱动开发》看了30遍,我很好奇的是,这30遍是在几年内看完的,看到第几遍的时候有种顿悟的感觉? 这本书我看了3,4遍,也实现了书中的例子,尽管我也看了几本相关的书,例如《重构》、《重构与模式》、《修改代码的艺术》、《敏捷软件开发》、《程序员修炼之道》等,但是TDD一直没掌握。 老师回答我的问题说:“还少27遍”,期待通过这个专栏能够让TDD成为我工具箱中的一项。应该就可以不用看30遍书了😁
2022-03-23 - 奇小易 👍(3) 💬(0)
2W2H笔记 Why Q1:为什么要学习TDD? 1、提高开发者的工程效能 工程效能包括(在开发过程中的) - 开发功能速度 - 定位问题速度 - 发现问题速度 - 解决问题速度 2、可靠、高效的职业程序员 What Q1: 程序员在所谓的"正常软件开发流程"中,都做过哪些测试?(或"程序员的测试"都有哪些?) a、验证性测试 what: 什么是验证性测试? 意图是为了验证代码实现是否符合功能要求。 故在明确的代码执行下,产生的执行结果,要验证该结果是否符合预期。 基于该意图所做的验证操作就是验证性测试。 具体例子如:(测试应用、跑一下) JPA示例中存储学生对象,最终预期就是将学生对象存入数据库中。 验证的方法就是验证下该对象是否真的存入了数据库。 而具体的验证手法可以通过人工查看的方式来实现,也可以通过编写测试代码来实现。 how good:验证性测试下有两种实现方式,分别是无计划的人工验证和有计划的自动化验证 b、定位性测试 what: 什么是定位性测试? 定位的意图是发现当前代码中存在问题,要快速定位问题,再来解决问题。 通常定位的手段是通过人工断点、打印等技巧,逐步分析错误范围,最终定位到问题,再加以解决。 而也可以通过给排查内容逐个增加验证测试,保证一个个测试通过的情况下,逐步缩小问题范围,从而定位到问题,再解决。 具体例子如:(调试) how good:定位性测试下有两种实现方式,分别是人工启发式定位和有计划逐模块自动化排查。 Q2:测试驱动开发的核心逻辑是什么? 构造软件的过程可以理解为,通过完成一系列的验证测试(完成需求),然后在验证过程中发现问题,再完成定位测试(解决问题),如此往复,最终直到完成全部功能。 在这样的过程中,测试构成了整个开发流程的骨架,功能开发可以看做是填充到测试与测试之间的血肉。 核心逻辑一:以测试为里程碑点的工程化研发流程(不太理解) 可以看出软件开发过程被分解为完成一个个测试的过程。基于这种分解,能够规划研发流程的进度。(这里不太理解) 核心逻辑二:可重复、高效的自动化测试代替低效的人工测试 可以看出,测试在软件开发过程中无处不在,如此重要的行为,需要高效实现,故需要用高效的自动化手段来代替低效的人工测试手段。 Q3: 课程包含哪些内容? 1、命令行参数解析的示例 2、测试驱动开发的核心理论和方法 3、能够说服他人践行TDD的理由 4、三个示例(ioc容器,restApi框架、db mapper框架) How Q1:如何学习TDD? A: 1、多练习,在不同场景下练习
2022-04-06