第 1 章 为何要关心测试驱动开发

第 1 章 为何要关心测试驱动开发

本书的作者是开发人员,针对的读者也是开发人员,因此大部分学习都将通过代码进行。每章都将介绍一个或多个TDD实践,读者将通过完成套路掌握它们。在空手道中,套路(kata)是一种练习,学习者不断重复同样的招式,每次重复都进步一点点。同样,读者阅读每章后,都将有细微但意义重大的进步。你将学习如何改善设计和代码、缩短上市时间、提供与时俱进的文档、通过质量测试提高代码覆盖率以及编写行之有效的清晰代码。

旅程都有起点,本书也不例外。我们的目标是让你成为拥有测试驱动开发TDD)黑带的Java开发人员。

为确定我们将走向何方,必须就一些决定航程的问题进行讨论并找到答案。何为TDD?这是一种测试方法还是别的什么东西?使用TDD有何好处?

本章旨在提供针对TDD的整体概括,帮你了解TDD定义及其优势。

本章涵盖如下主题:

  • 理解TDD;

  • 何为TDD;

  • 测试;

  • 模拟;

  • 可执行的文档;

  • 无需调试。

1.1 为何要使用TDD

你所处的环境使用的可能是敏捷开发方法,也可能是瀑布开发方法;你们公司可能有明确的规程,这些规程经过了多年艰苦奋斗的洗礼;也可能,你们只是一家刚刚起步的创业公司。无论如何,你都很可能面临过下述一个乃至更多痛点、问题或导致交付失败的原因:

  • 部分团队成员无缘参与需求、规范或用户故事的制定;

  • 大部分乃至全部测试都是手动的,抑或根本就没有测试;

  • 虽然使用了自动化测试,但并未检测出真正的问题;

  • 编写并执行自动化测试的时间太晚,无法给项目带来真正的价值;

  • 总是有更紧急的问题需要处理,没法腾出专门用于测试的时间;

  • 整个团队分为测试、开发和功能分析小组,而这些小组常常不能同步;

  • 无法重构代码,因为担心这样做会破坏既有的功能;

  • 维护成本高;

  • 上市时间过长;

  • 客户觉得交付的产品不符合要求;

  • 文档从来都不是最新的;

  • 害怕部署到生产环境,因为结果无法预料;

  • 常常无法部署到生产环境,因为运行回归测试的时间太长;

  • 团队为搞清楚某些方法或类的作用花费的时间太多。

测试驱动开发并不能神奇地解决所有这些问题,而只为我们找到解决方案指明方向。世上没有灵丹妙药,但如果有什么开发实践能让众多层面的情况大不相同,那就是TDD。

测试驱动开发可缩短上市时间、简化重构工作、帮助创建更好的设计以及降低耦合程度。

除这些直接的好处外,TDD还是众多其他实践(如持续交付)的前提条件。使用TDD可改善设计和代码的质量、缩短上市时间、确保文档最新、获得极高的代码覆盖率等。

要掌握TDD并不那么容易。即便学习了所有的理论,仔细研究了最佳实践和反模式,旅程也才刚刚开始。要掌握TDD需要很长的时间和大量的实践,这是漫长的过程,绝不是阅读完本书就能结束的。事实上,这个过程根本就没有结束的时候,因为总是有新的方式面世,让你能够更熟练、更快捷地使用TDD。然而,需要付出的代价虽然很高,但带来的好处更多。使用TDD的时间足够长的人都宣称没有其他开发软件的方法,我们就是这样的人,你肯定也会成为其中一员。

学习编码技巧的最佳方式是实践,我们对此深信不疑。要掌握本书介绍的内容,仅在上班的路上翻阅还不够;这不是一本适合躺在床上阅读的书,你必须撸起袖子动手编写代码。

本章将介绍基础知识,但从下一章开始,你将通过阅读、编写和运行代码进行学习。我很想说等阅读完本书后,你就是经验丰富的TDD程序员了,但情况不是这样的。读完本书,你将熟悉TDD,并拥有坚实的理论和实践基础,而剩下的事就全靠你自己了。要想获得更多TDD经验,你必须在日常工作中使用TDD。

1.1.1 理解TDD

此时你可能正自言自语:“我知道TDD会带来一些好处,但测试驱动开发到底是什么呢?”TDD是一种简单的流程,要求你先编写测试,再编写实现代码,这与“编写代码后再测试”的传统方法相反。

1.1.2 红灯-绿灯-重构

测试驱动开发是一个过程,依赖于不断重复极短的开发周期。它基于极限编程XP)的测试优先理念,倡导采用可高度信赖的简单设计。驱动这个流程前行的开发周期称为“红灯-绿灯-重构”。

这种流程本身很简单,由几个反复进行的步骤组成:

(1) 编写一个测试;

(2) 运行所有测试;

(3) 编写实现代码;

(4) 运行所有测试;

(5) 重构;

(6) 运行所有测试。

鉴于测试是在实现前编写的,因此它应该不能通过。如果通过了,就说明测试是错误的:要么它描述的功能早已存在,要么编写不正确。编写测试期间处于绿灯状态昭示着存在错报的问题,对于这样的测试,应将其删除或进行重构。

 编写测试时,应处于红灯状态。完成测试要求的实现后,所有测试都应通过,此时将处于绿灯状态。

如果最后一个测试未通过,就说明实现不正确,必须修正:要么这个测试不正确,要么实现代码不符合我们制定的规范。如果其他测试未通过,就说明我们破坏了某种功能,必须撤销所做的修改。

在这种情况下,一种自然而然的反应是:花足够的时间修复代码,让所有测试都通过。然而,这样的做法是错误的。如果不能在几分钟内完成修复,最佳的选择是撤销所做的修改。毕竟修改前一切都正常,带来破坏的实现显然是错误的。为何不到原来的地方,重新考虑实现测试的正确方式呢?这样我们只是在错误的实现上浪费了几分钟,而不会为修复一开始就不正确的东西浪费更多时间。原有的测试覆盖率(不包括最后一个测试的实现)应该很高。我们通过有意识地重构来修改既有代码,而不将其作为修复最近编写的代码的方式。

 不要试图让最后一个测试的实现完美无缺,而应只编写足以让这个测试通过的代码。

你可以任何喜欢的方式编写代码,但要快。一旦进入绿灯状态,我们就知道存在一个由测试构成的安全网,可接着重构代码了:改进和优化代码,但不引入新功能。重构结束后,所有测试应当在任何情况下都能通过。

如果重构期间有测试未通过,就说明重构破坏了既有功能,应像以前一样撤销所做的修改。在重构阶段,我们不修改任何功能,也不引入新的测试,而只改进代码,并不断运行所有测试,确保没有破坏任何功能。与此同时,我们证明了代码是正确的,并降低了未来的维护成本。

重构结束后,再重复整个过程。这是一个无限循环,每次循环都是一个极短的周期。

1.1.3 速度是关键

想想打乒乓球的情形吧。这项运动的节奏非常快,职业选手玩起来可能让人目不暇接,TDD与这项运动很像。TDD老手通常不会让接球(编写测试或实现的)时间超过一分钟:编写简短的测试并运行所有测试(乒),编写实现并运行所有测试(乓),再编写一个测试(乒),编写该测试的实现(乓),重构并确认所有测试都通过(计分);然后重复上述过程:乒、乓、乒、乓、计分。不要试图让代码完美无缺,而应力图让球不断运动,直到需要计分(重构)为止。

 测试和实现的切换时间应以分钟甚至秒计。

1.1.4 TDD并非测试方法

TDD中的T常常遭人误解。测试驱动开发是一种设计方法,要求在编写代码前考虑实现以及代码需要提供的功能,且每次只关注一项功能的需求和实现——这有助于理清思路以及更好地组织代码。这并不意味着使用TDD时编写的测试毫无用处,甚至恰好相反:它们很有用,让我们能够以极快的速度进行开发,同时不担心破坏既有功能。对重构来说这显得尤为重要:能够在不担心破坏既有功能的情况下重新组织代码对改善质量大有裨益。

 测试驱动开发的主要目标是提供可测试的代码设计,测试只是一项很有用的副产品。

1.2 测试

虽然测试驱动开发主要是一种代码设计方法,但测试也是其中一个很重要的方面,因此我们必须对如下两种测试方法有清晰的认识:

  • 黑盒测试;

  • 白盒测试。

1.2.1 黑盒测试

黑盒测试(也叫功能测试)将受测软件视为一个黑盒,无需知道其内部构造。这种测试是通过软件界面进行的,旨在确认它们像预期的那样工作。只要界面的功能未变,测试就应通过——即便内部构造发生了变化。测试人员知道程序该做什么,但不知道它是如何做的。黑盒测试是传统组织最常使用的测试类型。这种组织通常将测试人员划归到一个独立的部门——在测试人员不熟悉编程、难以理解代码时尤其如此。这种测试方法提供了外部观察受测软件的结果。

下面是黑盒测试的一些优点:

  • 可高效测试大块代码段;

  • 无需访问和理解代码,也不要求测试人员知道如何编写代码;

  • 将用户角度和开发人员角度分离。

下面是黑盒测试的一些缺点:

  • 覆盖率有限,因为只执行部分测试场景;

  • 测试效率低下,因为测试人员对软件内部构造一无所知;

  • 测试缺乏针对性,因为测试人员对应用程序的了解有限。

用于驱动开发的测试通常是根据验收标准进行的,而验收标准决定了要开发哪些功能。

 自动化黑盒测试依赖于某种形式的自动化,如行为驱动开发BDD)。

1.2.2 白盒测试

白盒测试(也叫透明盒测试、玻璃盒测试和结构测试)查看受测软件的内部,并将由此获得的知识用于测试过程。例如,如果在特定条件下应引发异常,可能需要在测试中重现这种条件。白盒测试要求测试人员了解系统的内部结构,同时具备编程技能;它提供了从内部观察受测软件的结果。

下面是白盒测试的一些优点:

  • 可高效找出错误和问题;

  • 知道受测软件的内部构造有助于进行详细测试;

  • 能够发现隐藏的错误;

  • 可帮助程序员反省;

  • 有助于优化代码;

  • 由于知道软件的内部构造,因此可最大限度地提高测试覆盖率。

下面是白盒测试的一些缺点:

  • 可能无法发现未实现或缺失的功能;

  • 需要对受测软件的内部构造有大致认识;

  • 需要访问代码;

  • 测试通常与产品代码的实现细节紧密耦合,导致重构代码后原本应该通过的测试未能通过。

白盒测试几乎都是自动化测试,且在大多数情况下都是单元测试。

 在实现前执行的白盒测试是以TDD方式编写的。

1.2.3 质量检查和质量保证的差别

还可根据要达成的目标对测试方法进行分类。要达成的目标通常有两种:质量检查QC)和质量保证QA)。质量检查的重点是发现缺陷,而质量保证力图将缺陷消灭在萌芽状态。QC是面向产品的,旨在确保结果符合预期;而QA更专注于过程以确保制造质量,即力图确保以正确的方式做正确的事情。

 质量检查过去扮演的角色更重要,但随着TDD、验收测试驱动开发ATDD)和行为驱动开发BDD)的面世,重点正转向质量保证。

1.2.4 更好的测试

无论使用黑盒测试、白盒测试还是两者兼而有之,编写测试的顺序都非常重要。

需求(规范和用户故事)是在实现需求的代码之前编写的,因此是它们定义了代码,而不是相反。对测试来说亦如此。如果它们是在代码之后编写的,那么从某种意义上说,是代码(及其实现的功能)定义了测试。由既有应用程序定义的测试有失偏颇,倾向于确认代码的功能,而不是检查客户的期望是否得到满足,或者说代码的行为是否符合预期。如果是手动测试,这种倾向可能不那么严重,因为手动测试通常由独立的QC(即使通常称为QA)部门来做。这种部门以独立于开发人员的方式定义测试,这将导致更严重的问题,因为必然会出现部门间沟通不畅和“警察综合征”的问题。所谓“警察综合征”是指,测试人员不力图去帮助开发团队编写有质量保证的应用程序,而只会在流程结束后找茬。问题发现得越早,为修复而付出的代价越低。

 以TDD(包括其ATDD和BDD等变种)编写的测试旨在未雨绸缪,将问题消灭在萌芽状态,确保开发的应用程序从根本上有质量保证。

1.3 模拟

要让测试能够快速运行并不断提供反馈,必须以合适的方式组织代码,以便能够轻松使用模拟对象(mock)和存根(stub)替换方法、函数和类。这种替换实际代码的方式通常称为“测试替身”。外部依赖可能严重影响执行速度,例如,代码可能需要与数据库通信。通过模拟外部依赖,可大幅提高速度。整个单元测试集的执行时间应以分钟乃至秒计。要想轻松使用模拟对象和存根,必须分离关注点以优化代码结构。

除可提高速度外,消除外部依赖还有其他更重要的好处。代码的外部依赖可能包括数据库、Web服务器、外部API等,这些外部依赖不但不可靠,而且访问需要很长时间。在很多情况下,这些外部依赖还可能不是现成的,例如,你可能需要编写与数据库通信的代码,并让人创建数据库模式(schema)。如果不使用模拟对象,就只能等到模式就绪后再测试。

 无论是否使用模拟对象,都应以合适的方式编写代码,以便能够轻松用一个依赖对象替换另一个依赖对象。

1.4 可执行的文档

TDD(以及更多结构良好的测试)另一个很有用的方面是文档。要搞清楚代码是干什么的,在大多数情况下通过查看测试比查看实现本身要容易得多。一些方法的作用是什么?查看与之相关联的测试。应用程序某部分UI的功能是什么?查看与之相关联的测试。以测试方式编写的文档是TDD的支柱之一,有必要更深入地了解。

传统软件文档存在的主要问题是,它们通常都不是最新的。一部分代码发生变化后,文档便不再反映实际情况。几乎任何类型的文档都如此,需求和测试用例受到的影响最大。

需要为代码编写文档通常意味着代码本身写得不好。另外,不管你如何努力,文档都必然会过期。

开发人员不应依赖于系统文档,因为它几乎在任何时候都不是最新的。另外,在详尽而及时地描述代码方面,没有任何文档比代码本身做得更好。

将代码用作文档并不意味着不能有其他类型的文档,关键是避免重复。如果说通过阅读代码可获悉系统细节,那么其他类型的文档可提供快速指南和概述。非代码文档应回答诸如“系统的总体目标是什么”“系统使用了哪些技术”等问题。大多数情况下,简单的README足以提供开发人员所需的快速入门指南;对新来者而言,项目描述、环境搭建、安装以及构建和打包说明等部分很有用。至于其他方面,代码就是“圣经”。

实现代码提供了所需的所有细节,而测试代码描述了产品代码背后的意图。

 测试就是可执行的文档,而TDD是创建和维护这种文档的最常用方式。

采用了某种持续集成CI)时,不正确的测试文档将失败并迅速得到修复。CI能够解决测试文档不正确的问题,但无法确保所有功能都有相关文档。有鉴于此(以及众多其他原因),应以TDD的方式创建测试文档。如果编写实现代码前,所有功能都以测试的方式做了定义,且所有测试都通过,测试便提供了完整而最新的信息,可供开发人员使用。

对于团队的其他成员,该如何办呢?测试人员、客户、产品经理等都不是程序员,可能无法从产品代码和测试代码获取所需的信息。

前面说过,最常见的两种测试是黑盒测试和白盒测试。这种划分很重要,因为这也将测试人员分成了两类:知道如何编写或至少阅读代码的(白盒测试),不知道如何编写和阅读代码的(黑盒测试)。有些测试人员两种测试都能做,但通常都不知道如何编写代码,因此对开发人员来说很有用的文档对他们来说毫无用处。如果需要将文档与代码分开,单元测试并不是很好的选择,这正是BDD横空出世的原因之一。

 BDD可在保留TDD和自动化的优点的同时,提供非程序员所需的文档。

客户必须能够定义系统的新功能,还必须能够获取有关系统各重要方面的最新信息。因此文档的技术性不能太强(将代码作为文档不可行),同时必须在任何情况下都是最新的。BDD叙述(narrative)和场景(scenario)是提供这种文档的最佳方式之一。BDD故事可作为验收标准(在代码之前编写的),可频繁执行(最好每次提交时都执行),还是使用自然语言编写的,因此不但在任何情况下都是最新的,而且可供那些不想研究代码的人使用。

文档是软件不可分割的一部分,与代码一样需要经常测试,这样才能确保它既准确又是最新的。

 要提供既准确又是最新的信息,唯一划算的方式是使用可集成到持续集成系统的可执行文档。

作为一种方法论,TDD提供了实现这种目标的良好途径。在底层,单元测试是最佳的可执行文档;而在功能层面,BDD是提供可执行文档的不错方式,它使用自然语言,确保了这种文档易于理解。

1.5 无需调试

作者几乎从未调试过自己编写的应用程序!这好像不可思议,但情况确实如此。我们几乎从不调试应用程序,因为没有理由这样做。在编写代码前编写测试且代码覆盖率很高的情况下,我们完全可以相信应用程序将像预期的那样工作。这并不意味着使用TDD编写的应用程序没有bug——bug肯定是有的,所有应用程序都有bug;但出现bug时,可轻松地找出它们——只需查看未被测试覆盖的代码即可。

测试本身可能没有涵盖某些情形。在这种情况下,应对措施是编写额外的测试。

 代码覆盖率很高的情况下,与逐行调试直到找到罪魁祸首相比,通过测试找出导致bug的原因要快得多。

1.6 小结

通过阅读本章,你对测试驱动开发实践有了大致的认识,还知道了什么是真正的TDD。你了解到TDD是一种代码设计方法,这是通过简短而可重复的“红灯-绿灯-重构”周期进行的。

整个TDD过程中,“测试未通过”都是一种意料之中的状态,你不但应该欣然接受,还需想办法进入这种状态。“红灯-绿灯-重构”周期很短,从一个阶段切换到另一个阶段的速度极快。

虽然主要目标是代码设计,但TDD过程中创建的测试是宝贵的财产,应充分利用,它们还会严重影响我们对传统测试实践的看法。对于这些实践,我们对其中最常用的几个(如黑盒测试和白盒测试)做了简单的介绍,试图从TDD的角度审视它们,并指出了它们带来的好处。

你发现模拟对象是非常重要的工具,对编写测试来说常常必不可少。最后,我们讨论了可以也应该将测试用作可执行的文档,还有TDD如何极大地减少了调试的必要性。

介绍必要的理论知识后,下面搭建开发环境、概述并比较各种测试框架和工具。

目录

  • 版权声明
  • 前言
  • 第 1 章 为何要关心测试驱动开发
  • 第 2 章 工具、框架和环境
  • 第 3 章 红灯-绿灯-重构——从失败到成功再到完美
  • 第 4 章 单元测试——专注于当下而非过往
  • 第 5 章 设计——难以测试说明设计不佳
  • 第 6 章 模拟——消除外部依赖
  • 第 7 章 BDD——与整个团队协作
  • 第 8 章 重构遗留代码——使其重焕青春
  • 第 9 章 功能开关——将未完成的功能部署到生成环境
  • 第 10 章 综述