【融云分析】关于测试驱动开发(TDD)的感悟

【融云分析】关于测试驱动开发(TDD)的感悟

提起测试驱动开发(以下简称“TDD”),圈内工程师对其都有一定程度的了解,TDD 的优点也得到了普遍的认可。有研究机构曾对微软和 IBM 的八个开发小组进行了对照测试,结果发现使用了 TDD 的小组比未使用的小组在问题发生比例上减少了四至九成。在结对编程的相关实验中,使用 TDD 的小组比未使用的小组在黑箱测试通过率上平均高出 18%,效果相当明显。

但反观日常所接触的实际工作中,对于 TDD 的使用并不多见,码农们对其价值也褒贬不一,网上甚至有大咖写文章公开论述观点来反对它,这又是为什么呢?前不久我刚好在融云的一个项目里小范围实践了一把,我就试着结合各方观点,斗胆谈谈自己的体会。

首先介绍一下什么是 TDD,TDD 最早是由 Kent Beck 提出并在他的《Test-Driven Development By Example》一书中进行详细阐述的(Kent Beck也是极限编程 Extreme Programming 概念的提出者)。书中所述:TDD 就是以测试作为开发过程的中心,要求编写任何产品代码之前,首先编写用于定义产品代码行为的测试,而编写的产品代码又要以使测试通过为目标的软件工程方法,目的是构造简单、清晰、高质量的代码。

测试是保证软件质量的重要手段,一个公司的研发部门通常都会有很大比例的测试团队作为质量保证的坚实后盾。那么作为开发人员是不是就可以只关心写代码的进度,编译通过后再跑几遍没问题就提交代码呢?这样的程序在碰上初始条件稍有改变,或者压力、并发等外部因素略有变化下会不会出现崩溃等问题呢?有的团队会制定规章制度,要求给完成的代码编写测试用例,必须通过才可以提交。这或许能在一定程度上使情况得到改善,但实际效果真的有那么明显么?我就听过有人抱怨说,完成代码后再写测试用例是浪费时间,因为对逻辑走向已十分了解,测试自然不会有什么大的纰漏,有那个时间不如多写几行代码,反正有测试团队兜底。乍一听似有道理,但仔细一想实则是在逃避责任,也在浪费公司资源。

那保证软件质量,有何良策呢?让我们来看 TDD 是如何做的,前面说过 TDD 的精髓在于将测试前置,这看似微不足道的变化到底会带来怎样的化学反应呢?

第一,能够明确目的。在动手编写生产代码之前,就得先想好这一部分逻辑的输入输出是什么,编写满足需求的测试用例,同时增加对需求的强化理解。举个简单的例子,与合作开发的同事对好需求,划分好各自实现的逻辑块,结果后续联调时才发现一方理解错了意思,之前的工作量就白费了。有了这提前编写的测试代码,就能逻辑层面再次明确目标,避免语言文字上的误解。在编写代码过程中,更容易做到心无旁骛,思绪不会乱飘,因为你的目标就是编写能通过测试用例的代码。当然这一过程可能会需要持续对测试目标进行完善,即所谓的 TDD 微循环:测试 -> 实现 -> 重构 -> 测试 …

第二,强化了模块与接口的概念。再纷繁复杂的业务逻辑也能按功能、层级分为若干业务模块,模块与模块之间通过接口(API)通信,做好这两点的设计无形中也就降低了业务逻辑的耦合性,低耦合又是单元测试的前提条件。这样操作等同于迫使开发者将接口的设计与低耦合性放在第一位去考虑。工作中在接到项目、明确需求后,如何分配任务往往非常考验团队人员的综合能力,拆解颗粒度太粗容易造成边界不明确;太细又牵扯过多精力,不易实施。但无论怎样,模块化和明确的接口设计是任务拆解得以顺利实施的前提,对将来可能发生的重构也是极好的。

第三,有利于任务的并行展开。当任务拆解分配到个人后,必然有些逻辑是需要前提输入条件的,比如客户端需要请求服务器,那么在编写测试用例时就应当提供这样的前提条件模拟,即 Mock 对象。目前流行的测试框架都带有 Mock 组件,比如谷歌的 GTest/GMock。有了这些交互对象,无论处在业务流程的哪个阶段,都可以马上展开任务,随时与上下游模块进行联调,同时利用各自的单元测试划分 Bug 归属。如果这一步不提前进行,开发任务就要按顺序进行,难免出现人员等待的情况。

第四,可以非常高效地进行重构。说起重构,可以说稍大点的项目,因为需求的变化或者逻辑的更新,重构在所难免。有些人就会心里发怵,生怕会引入新的 bug,甚至陷入重构的泥潭。如果这时有了事前准备好的测试用例,每重构完一块查看一下测试结果,就可化风险于无形。

此外 TDD 还很多优点,如快速反馈,测试用例即文档,降低测试团队负担等等。聊完了优点,接下来再看看网上大咖们对 TDD 的负面情绪都有哪些。

目前来看最集中的抱怨就是 TDD 会增加时间的投入。TDD 的优势是建立在高质量测试用例这一前提下的,如果测试代码写的不够好或者不够全面就难以覆盖所有功能点,而“测试 -> 实现 -> 重构”的微循环也会带来不少测试代码的开发工作。对于新手来讲,耽误了时间,效果却很有限,自然动力不足。即便是有信心能使用好 TDD 的团队,很多时候它的付出回报比也依然不高,尤其在强调迭代速度的互联网公司,根据时间、质量、花费三者只能取其二的理论,多数情况下也只能舍弃一部分质量来保证研发速度,何况有经验的团队即使不用 TDD 也是能保证质量损失在可控范围内的。

其他的抱怨来自 TDD 的规则太教条化,比如它的三大定律:

1. 在编写不能通过的单元测试前,不可编写生产代码。

2. 只可编写刚好无法通过的单元测试,不能编译也算不通过。

3. 只可编写刚好满足以通过当前失败测试的生产代码。

这一点关键看怎么理解,有个博主说的就很好 “Learn the rules, THEN break them.” 一旦理解了定律所表达的真实含义,是可以根据实际情况灵活变通的。

下面来谈谈个人对 TDD 一些肤浅的看法。

既然 TDD 是软件工程的一种理论方法,那么就会有它的适用范围,短平快的任务应用空间不会太大,TDD 更适合一些大中型、需要长期维护的项目,最好团队中能有 TDD 经验的人带领,如果没有可以去看看网上一些著名的开源项目是怎么编写它的测试代码的,学习高手写的代码,每看一次都非常受益。

另外极限编程的一些理念,比如 KISS(Keep It Simple, Stupid),YAGNI(You Aren’t Gonna Need It)与 TDD 结合起来也很值得玩味。这也不难理解,XP 和 TDD 本来就是一个人提出的理论。无论写代码还是写测试用例,这些原则性的东西,最好能贯彻。

最后再说一些编程方面的经验吧,最近这个项目给我印象最深的就是断言的使用。不仅是在测试用例中用来判断结果与期望值是否相符,更多是要在程序的关键位置埋好断言,将风险扼杀在摇篮之中。比如开源代码 WebRTC 在核心类的方法中就大量使用了断言,判断调用线程是否正确,关键值是否符合要求等。这类错误可能会在程序运行到后面某个点才暴露出来,相同原因导致的现象多种多样,如果不及时发现,会大大增加 Debug 的成本。

所以我的建议就是断言要大胆的加,甚至允许在 Release 版本中的关键位置存在断言,这样用户在反馈问题时,就能正确归因,及时解决改进。

       

标签: , ,