测试驱动开发(TDD)知识调研
文章内容摘自 C++程序设计实践与技巧——测试驱动开发 (Modern C++ Programming with Test-Driven Development:Code Better,Sleep Better) , Jeff Langr著
TDD的核心目标
TDD的核心目标就是尽可能频繁地获得较多的反馈。当你修改了一点代码时,会想马上知道改动是否正确。
你每做出一次小的改动,都需要运行所有的单元测试。TDD最大的好处是它能让你在短时间内获得有用的反馈。如果构建的测试运行得快,那么如前所述,完全有可能在几秒内运行完所有的测试。如果等待的时间足够短,那么经常运行所有的测试也是合理的。一旦反馈周期变长,TDD的威力就会减弱。获取反馈的间隔时间越长,那么你写的代码出问题的可能性就越大。
通常在编写代码时很容易引入或大或小的问题。相比之下,每做一个小改动就运行一次测试,就能逐个解决这些问题。
处理遗留代码问题的核心法则如下。
-
任何时候,只要可以就进行测试驱动。使用测试驱动方法时,有可能的话就将需要改动的代码作为新的成员或新的类。 -
不要让测试覆盖率缩水。 非常容易出现的情况是,改动一点代码,然后认为这只是一些简单的代码行,并对之不予过多考虑。如果没有测试,每一行新加的代码都会导致测试覆盖率降低。 -
为了编写测试,必须改动现有代码! 由于对协作对象的依赖,大多数情况下不能方便地为遗留代码编写测试。在编写测试前,需要一个打破依赖的方法。 -
可以在限制范围内实施微小的代码改动,这样会降低风险。用一些技巧就可以手动地做出一些小而安全的代码变换。 -
你编写的每行代码都有风险,甚至一个敲错的字符都会引入潜在的缺陷,而这可能会浪费好几个小时。尽可能少写代码,每敲击一次键盘时都要想清楚。 -
坚持小的、增量的改动。 这在TDD中是奏效的。同样,对遗留代码也是如此。步子迈得太大会使你陷入困境。 -
一次只做一件事。在处理遗留代码时,不要合并步骤或目标。例如,不要在重构的同时写测试。 -
有些增量的改动可能会使代码变得丑陋,要接受这一点。记住,一次只做一件事。 可能需要几个小时做“正确”的事。不要等,现在就提交工作方案,因为这样或许就不用花费过多时间。同时,不要过于担心会违反一些设计原则。最终,你可能要抽出时间回过头来整理代码,或许你不会,但依然是进步了。你已经向正确的方向迈进了,而且也证明了所有代码依然可以工作。 -
增量地修改代码。 面对庞大的代码库时,仅仅为遇到的相关代码编写测试或许不能带来巨大改观。更重要的是你内心深知任何新加入的东西都需要经过测试。秉承测试第一的心态,你会开始从加入的测试中获益。 每通过一个测试,可以回顾一下测试覆盖的代码区域。几乎总能发现做一些小的、安全的重构工作的机会。同时,你也会发现在写完一个测试后,再写一个是如此容易。你可以在各个地方应用这种小的改善步伐而丝毫不影响产出,也将在困难重重的代码库中如履平地。
不良测试的死亡漩涡(亦称为 SCUMmy 周期)
有时团队开始使用TDD后,会在一段时期内得到良好的效果。而后事情开始慢慢被忽略,然后快速被忽略,最终决定放弃TDD。是什么导致了这个“讨厌的测试死亡漩涡”?怎样才能避免它呢?
这个问题不仅仅存在于TDD中。同样也有“讨厌的敏捷死亡漩涡”,伪敏捷的短迭代周期似乎在一段时间内产生了良好的效果。但1年或18个月后,团队会对手头上一堆混乱的场面十分吃惊。其结果就是敏捷方法被抛弃,背负着浪费时间的骂名。
Ben Rady和Rod Coffin在Agile2009峰会上名为“Continuous Testing Evolved”①的演讲中描述了SCUMmy周期。缩写SCUM描述了以下几种导致退化的不良测试的特征:慢速(slow)、令人疑惑(confusing)、不可靠unreliable)、遗漏(missing)。下面是一个可能走向漩涡的情况(我在一些团队中看到过几次此类情境)。
-
团队写的测试大部分是集成测试。这些测试和不稳定或慢速依赖紧密耦合,如数据库或其他外部API。虽然这会导致更慢的测试,但开始不会觉得有很大影响,因为仍可以在1~2分钟内运行完几百个测试。(想想“温水煮青蛙”。) -
测试变多后带来的问题超越心理承受底线。现在运行完测试需要好几分钟。 -
开发人员运行测试的频率变低,或者只运行一个测试子集。同时,团队成员发现运行测试的问题增多。测试变得更长,需要更多的必要初始化,一旦出现问题,则需要更多的精力来进行理解和分析。其他问题也开始慢慢显现,由于依赖单元测试控制之外的不稳定因素,测试会间歇性地失败。开发人员发现测试经常上演“狼来了”,这意味着问题不是出在产品系统自身,而是测试设计。 -
开发人员删掉测试。对于有问题的测试,本能反应就是禁用甚至删掉。开发人员发现,删掉测试比花费一个小时修复它们更容易。 -
代码缺陷开始变多。剩下的测试很可能覆盖不了足够多的逻辑,在防止代码缺陷方面价值较小。(在文档化价值方面也大打折扣。) -
团队或管理层质疑TDD的价值。团队试图继续,但很明显这是徒劳的。 -
团队放弃TDD。管理层记下这一明显的失败。
团队该怎么办呢?如果早知如此,那又何必当初呢?
理想状况下,你已经从一些资料中学到了TDD,它要求限制每个测试的范围,只测试一小段独立的逻辑。以这种方式构建的系统不太可能卷入不良测试的死亡漩涡。而且,不是用了TDD就能神奇地产生高质量的系统。你和团队必须不遗余力地去掉测试和产品代码中的不良设计。这同样要求团队知道良好的测试和代码是什么样子的。
警惕撤回迈入死亡漩涡的步伐
下面的步骤将撤回迈入漩涡的步伐。如果你积极地观察发生的事情,就可能不会再次在漩涡中越陷越深。 (1) 团队写的测试大部分是集成测试。学习怎样编写单元测试。重读本书、参加培训、雇一个教练、举办Dojos、更多地审查、更多地阅读,等等。同时也要增加关于良好设计及代码结构的知识。
(2) 测试变多后带来的问题超越心理承受底线。将测试分为慢速和快速测试集。设立快测试的标准。(在一台开发机上需要5毫秒或更少时间?)如果团队成员向慢速测试集中加入一个测试,需要告知大家。学习重构测试及相关代码要做的事情,以便让测试变快。养成习惯,增量、但经常地尝试将慢速测试改进为快速测试。
(3) 开发人员运行测试的频率变低,或者只运行一个测试子集。 如果测试运行超过慢速阈值,那么将测试集标为失败。(我最近为客户成功地改动Google Test做到了这点。)这样做会强化开发团队认识到快速测试的重要性。
(4) 开发人员删掉测试。监测测试覆盖率。虽然建立覆盖率目标的价值有待商榷(参见11.6节),你还是倾向于不断增加或至少有一个稳定的覆盖率数据。不幸的是,用良好的单元测试代替不良测试并获得同样的覆盖率可能要费点功夫。但在养成正确的习惯前,最好花点时间往对的方向走,而非放弃。
(5) 代码缺陷开始变多。对一个代码缺陷的首要任务是写一个测试。代码缺陷为认识TDD实践中的不足提供了机会。对于每个缺陷,要坚持在修复问题前写一个运行失败的单元测试。
(6) 团队或管理层质疑TDD的价值。精益求精。坚信TDD实践及其他实践(如验收测试、重构和结对编程)从长远来看主要致力于产出高质量的软件,而非仅仅减少代码缺陷。确保明白这些实践如何以及为什么和质量挂钩,同时确保团队成员以一种有助于达成高质量目标的方式践行它们。缺乏对质量的关注将导致系统开发失败——“不良测试的死亡漩涡”更不可饶恕。 (7) 团队放弃TDD。不要坐以待毙!管理层很少会容忍再次迈向他们认为必定失败的步伐。和其他事情一样,你可能会因不当地使用TDD而导致失败。但也有可能成功,并且是大获成功,否则的话,我也不会大动干戈地写下本书。由于未能正确地坚持一项技术而认为它本身不好,可是非常不好的!
测试先行 – FIRST原则
可以对照FIRST原则(由Brett Schuchert和Tim Ottinger提出)来审查。这个助记符可以提醒你TDD定义中的关键部分:测试先行。
FIRST可以分解为如下部分:
-
F 是快速(Fast); -
I 是独立(Isolated); -
R是可重复(Repeatable); -
S 是自我验证(Self-verifying); -
T 是及时(Timely)
|