测试驱动开发(TDD)始于上世纪 90 年代,时至今时今日,依然只有少数的开发者在践行着。本文作者从软件开发者的角度,又一次帮助我们定义了测试驱动开发,解答了众多开发着对 TDD 常见的谬误。
作者 | Tylor Borgeson,已获作者翻译授权
译者 | 罗昭成
原文 | “Test Driven Development is overrated”
本文首发于 CSDN 微信(ID:CSDNnews)
这是我「流行软件开发实践」系列文章中的第二部分,在本系列文章中,我计划包含软件工程师通过提升开发流程和实践来改善软件开发的一系列方法。我曾在 ThoughtWorks 担任软件顾问,现在我在德国一家大型的零售公司工作,这些方法都是我在职业生涯中学习并实践验证过的。
我曾和一位客户的开发人员讨论过有关软件开发方面的问题。我们的讨论有点过头,他提到,作为一个软件开发者非常地幸运,因为“我们可以欺骗公司为我们的基础工作付出可观的收入”……我认为,不论你的编程水平如何,你也不能如此贬低众多软件开发工程师。
我们聊得如丝般顺滑,讨论逐渐深入到敏捷软件开发实践上。对于敏捷开发中提到的方法,他们表示可以进行尝试,用于改进当前的工作方式,但是,当我提到测试驱动开发的时候,他们却都觉得:
测试驱动开发名不副实。
听到他们的这个评论,我感到非常震惊。同时,他们也让我意识到,在很多人心中,测试驱动开发仅仅是敏捷开发实践中的一种方式,在更多人心里,它更像是海市蜃楼,看起来很美,用起来却不过如此。为了让更多的人了解测试驱动开发,我想用本篇文章告诉大家到底什么是测试驱动开发。
首先,我来定义一下什么是测试驱动开发(TDD)。
顾名思义,TDD 是一种软件开发的策略,它通过写测试来引导开发流程。Kent Beck 2003 年在他的《测试驱动开发》一书中提到了这个概念(译者注:Kent Beck,美国著名软件工程师与作家,在软件工程方面有很大贡献。他是 Smalltalk 软件的开发者,设计模式的先驱,测试驱动开发的支持者,也是极限编程的创始者之一。),实现测试驱动开发主要遵循以下三个步骤:
为你要写的一小部分功能编写一个失败的测试用例。
实现你的功能逻辑,让你的测试用例通过。
重构代码,保证代码的结构与可读性。
这个流程可以简化为“红色,绿色,重构”。
下面,我将用测试驱动开发的模式,用 Python 实现勾股定理中斜边值的计算。
今年已经是 2020 年了,17 年过去了,对于很多开发者而言,测试驱动开发带来的收益依然没有定论,因此,大多数开发人员都不曾使用过 TDD 。
一直以来,我都热衷于向我的朋友们推荐使用 TDD ,但依然有很多人选择拒绝,当我问到为什么时,以下是我听到的最多的几个回答:
我们有测试团队,写测试是他们的工作。
在写测试用例的时候,需要 mock 上下文对象,所以需要花费更多的时间和精力。
并不能给我带来很大的收益。
它太慢了。
下面,我来分析一下这几个回答。
我一直觉得,谁写的代码,谁就应该要保证它能正常运行,所以我每次想到这个答案我都会觉得好笑。如果你觉得只有写逻辑代码才是你的工作,那么你的能力将会停滞不前。
每一个程序员写的代码应该都要具备以下几个性质:
根据业务需求选择使用正确的技术栈
代码容易理解
可测试
可扩展
简单
我认为,把开发者能做的单元测试交给测试部门来做,是非常不明智的行为。测试部门有更多更重要的事情要做,而不是浪费时间来做黑盒测试。
你们的想法和无奈感,我都能理解,曾经的我也和你们一样。在你的面前有一个方法,这个方法有三个不同参数,并且每一个参数都是一个对象,同时,它们的属性也不为空。现在你需要对这个方法进行测试,要满足测试条件,需要进行大量的 Mock,这会导致为了测试一个方法而写大量的额外代码。
现在,我已经能够很好的处理这个问题,并且我认为做这些事情都是值得的。
每当我思考如何降低写测试的成本时,脑海里就会萦绕着这两样东西,分别是SOLID(译者注:S 指单一职责原则,O 开闭原则,L 里氏替换原则,I 接口隔离原则,D 依赖倒置原则)和测试金字塔。
需要强调的是,S —— 单一职责原则,这个原则告诉我们,一个类或者一个方法都只能做一件事情,这样可以防止它们对系统的其他部分产生副作用。
我知道,公司中的代码逻辑肯定不会像两个整数相加那么简单,但是,如果我们可以让这些方法的职责变得单一,那么写测试也就会变得非常容易。
△测试金字塔,来源:martinfowler
这个测试金字塔通常用来衡量不同测试类型的优先级。
UI 测试和服务测试会耗费大量的时间,成本很高,单元测试却可以毫秒级地运行,所以一个系统应该让它具有更多的单元测试,少一点服务测试和 UI 测试。这样做和 DevOps 的目标一致,可以增加整个系统开发过程中的反馈循环。
单元测试要求新的代码与系统中原有的代码尽可能少的耦合在一起,这样能更容易编写单元测试的代码。
测试驱动开发是基于单元测试的一种开发模式。
当然,说起来比做起来容易得多。对于各种类型测试来说,都会有成本,单元测试也不例外。如果有一天,你发现你写单元测试的成本增加了很多,你可以扪心自问,是否有以下两个问题:
当前的测试是否在单元测试范围内,是否需要把升级到服务测试的范围?
你的代码结构是否合理,你的类和方法是否单一职责?
如果你在编写单元测试代码时,都需要不断的投入大量成本,这就意味着,你的代码存在一些问题,你需要对其进行一些重构,让代码结构更清晰。
很搞笑的是,大多数这么说的人,都是没有真正使用测试驱动开发模式的人。纸上得来终觉浅,绝知此事要躬行。
下面,我将介绍使用 TDD 带来的两大好处:
第一个比较明显,根据 TDD 的定义,在编写真实的逻辑代码之前,都需要写先测试,以这种方式产出的代码都是自测试的。正如 Martin Fowler(译者注:Martin Fowler,英国著名软件工程师,也是一个软件开发方面的著作者和国际知名演说家,专注于面向对象分析与设计,统一建模语言,领域建模,以及敏捷软件开发方法,包括极限编程。)所说:
如果你的代码是自测试的,那么只要你针对代码运行一系列自动化测试,等到通过后,你就可以保证你的代码不会出现任何实质的缺陷。
换句话说,你的代码可以按照你在测试代码中定义的流程正常运行。
单元测试非常的有用,它能让你对你的代码充满信心。在重构的时候,它能在第一时间发现重构造成的问题,并且,并且单元测试也可以让你知道当前系统中是否存在 Bug 。
自测试的代码可以让整个团队在开发和集成的过程中受益。
使用 TDD 另一个好处是,它能让开发人员在编写逻辑代码之前,仔细思考要编写什么样的代码,以及如何去编写代码。在编码之前的思考,能够让开发者真正深入业务需求,考虑可能存在的边界问题和可能存在的挑战,这个过程虽然费时而且很痛苦,但它并不是浪费开发人员的精力。不仅如此,先写测试,还会促使开发人员从一开始写代码的时候,就考虑系统的设计与架构,这样可以大大地提高系统扩展性。
以我的经验来看,有这种想法的人,大多都是那些尝试使用 TDD 的人,然而他们去使用的是以前开发模式,先写代码,最后在来编写测试。这也是大多数开发人员不能坚持执行 TDD 最常见的原因之一。
我深深地理解这些开发者的想法,在编写代码之前,先进行思考,然后花时间去写测试代码,比起不写测试,确实要花费更多的时间。有时候,模拟 mock 出场景有效的数据不仅比较麻烦,也会让人感到很沮丧。
虽然我说很多 TDD 的优点,但是这无济于事,很多公司的业务部门都将开发交付的时间作为衡量标准,整个团队都在拼尽全力开发新的需求。
在敏捷开发中,衡量软件开发有四个关键指标(我经常会参考这些指标),其中两个为变更失败率和部署频率。正如我上面所说,假设系统使用 TDD 构建,当你提交代码,整个团队都会知道代码的修改是否会导致系统其它功能崩溃,同时还能提高 Bug 被发现的速度。单元测试的局部性,可以让开发人员可以快速的找到问题出现的原因。如此往复,导致功能出现异常的可能性会越来越小,从而降低变更失败率。
代码通过单元测试,也可以让我们对软件功能的鲁棒性有更强的信心,整个团队都可以通过单元测试结果来决定是否可以部署,任何人都可以按需进行部署。
《凤凰项目》中列出了四种工作类型,其中一种是“计划外工作或救火工作”。这类工作通常是由于错误引起的,开发者必须停止手上的工作去优先处理。自测试的代码可以最大的程度上减少这种错误,这就意味着,它能帮助我们最小化“计划外工作或救火工作”,还能最大限度的提高开发人员的满意度。快乐的开发就是好的开发。
当开发者不用担心线上会出现问题的时候,他们能够更加聚焦在新功能的开发上。他们在写代码之前,有考虑过如何写代码,便能够写出符合 SOLID 原则的代码。在开发中会不断地对代码进行重构,技术债也会降到最低。团队对代码质量充满信心,能随时进行上线部署,这会使整个团队跑得更快。
万事开头难,和其他工具和实践一样,测试驱动开发在刚开始的时候,可能会让人很难受,并且会让你的开发效率降低,但是,正如持续交付之父 Jez Humble 所说:
提前并频繁地做让你感到痛苦的事。
现在去开始你的测试驱动开发之旅,直到它让你觉得舒服为止。谢谢你的阅读。
引用:
Continuous Integration and Deployment
Accelerate, The Phoenix Project, and DevOps
SOLID Principles
“You build it, you run it”
Martin Fowler — Self Testing Code
Martin Fowler — Testing Pyramid
1. 为什么持续集成和部署在开发中非常重要?
2. 被高估了的测试驱动开发?
3. 为什么程序员如此“嫌弃”主干开发模式?
4. 程序员为什么千万不要瞎努力?
5. 为什么许多程序员讨厌结对编程?
6. 程序员如何用代码彻底终结系统那些事儿?