优质代码:软件测试的原则、实践与模式

978-7-115-37558-2
作者: 【美】Stephen Vance
译者: 伍斌
编辑: 杨海玲

图书目录:

详情

本书讲述如何对所有的软件进行轻松的例行测试,书中为读者提供一些工具——一些实现模式,这些工具几乎可以测试任何代码。本书分为三个部分:第一部分讨论了测试的一些原则和实践,包括首次优质、代码意图、测试攻略和测试与设计之间的关系等;第二部分讨论了有关测试实践方面的一些模式;第三部分展示了两个实例的编程过程。

图书摘要

软件开发方法学精选系列

Quality Code:Software Testing Principles,Practices,and Patterns

优质代码 软件测试的原则、实践与模式

[美]Stephen Vance 著

伍斌 译

人民邮电出版社

北京

图书在版编目(CIP)数据

优质代码:软件测试的原则、实践与模式/(美)万斯(Vance,S.)著;伍斌译.--北京:人民邮电出版社,2015.1

(软件开发方法学精选系列)

书名原文:Quality code:software testing principles,practices,and patterns

ISBN 978-7-115-37558-2

Ⅰ.①优… Ⅱ.①万…②伍… Ⅲ.①软件—测试 Ⅳ.①TP311.5

中国版本图书馆CIP数据核字(2014)第273844号

内容提要

本书讲述如何对所有的软件进行轻松的例行测试,书中为读者提供一些工具——一些实现模式,这些工具几乎可以测试任何代码。本书分为三个部分:第一部分讨论了测试的一些原则和实践,包括首次优质、代码意图、测试攻略和测试与设计之问的关系等;第二部分讨论了有关测试实践方面的一些模式,包括测试构造器和getter/setter、处理宁符串、封装与覆写、调整代码可见性、测试单例模式、验证错误条件,以及利用各种接缝和测试多线程等;第二部分展示了两个实例的编程过程,其中一个是用测试驱动开发方法编写新的Java应用程序WebRetriever,另一个是为一个未写测试的JavaScript开源项目iQuery Timepicker Addon添加测试代码。

本书适合对测试驱动开发有初步了解或实践并想提升测试代码编写技能的程序员和自动化测试工程师阅读,也适合想通过本书在GitHub上的微量提交的代码来学习用测试驱动开发方法编写Java新项目和用测试来驯服JavaScript遗留代码的详细过程的任何读者阅读。

◆著 [美]Stephen Vance

译 伍斌

责任编辑 杨海玲

责仟印制 张佳莹 彭志环

◆人民邮电出版社出版发行  北京市丰台区成寿寺路11号

邮编 100164  电子邮件 315@ptpress.com.cn

网址 http://www.ptpress.com.cn

北京铭成印刷有限公司印刷

◆开本:720×960 1/16

印张:13.5

字数:241千字  2015年1月第1版

印数:1-4000册  2015年1月北京第1次印刷

著作权合同登记号 图字:01-2014-1729号

定价:49.00元

读者服务热线:(010)81055410 印装质量热线:(010)81055316

反盗版热线:(010)81055315

版权声明

Authorized translation from the English language edition,entitled Quality Code:Software Testing Principles,Practices,and Patterns,9780321832986 by Stephen Vance,published by Pearson Education,Inc.,publishing as Addison-Wesley,Copyright © 2014 by Pearson Edu cation,Inc.

All rights reserved.No part of this book may be reproduced or transmitted in any form or by any means,electronic or mechanical,including photocopying,recording or by any information storage retrieval system,without permission from Pearson Education,Inc.

CHINESE SIMPLIFIED language edition published by PEARSON EDUCATION ASIA LTD.and POSTS & TELECOM PRESS Copyright © 2015.

本书中文简体字版由Pearson Education Asia Ltd.授权人民邮电出版社独家出版。未经出版者书面许可,不得以任何方式复制或抄袭本书内容。

本书封面贴有Pearson Education(培生教育出版集团)激光防伪标签,无标签者不得销售。

版权所有,侵权必究。

译者序

2014年仲夏前的一个下午,当我正在为自己的处女作《驯服烂代码》的收官忙得焦头烂额时,接到了策划编辑杨海玲老师发出的翻译本书的邀请。由于之前翻译过海玲老师策划的《测试驱动数据库开发》,了解译书的辛苦意味着什么。看了本书的页数,我就知道,要翻译它需要我在写书的同时,额外再花3个月每天用五六个小时来翻译。我想时间真的不够用,要是这书不合我的口味,就找个理由谢绝吧。于是就在亚马逊网站上找到了本书的目录和书评。

本书在亚马逊网站的评价是4颗星,一个不错的分数。再看给最高星级的评价说本书中的例子很清晰。嗯,我喜欢讲述清晰的作品。

再看看目录吧。这一看不要紧,本来是要找不翻译本书的理由,结果反倒找到了下面这些对我胃口的关键词:软件匠艺、首次优质、代码意图、单例模式测试、错误条件验证、接缝的使用、多线程测试等。这些关键词成了我要译本书的理由。

回想我自己写的书,在讨论测试技术实现的广度和深度方面与本书相比真是望尘莫及。比如前面提到的单例模式测试、错误条件验证和多线程测试,都是我基本从未考虑过的。由此可见,本书作者在测试驱动开发领域的实践方面功力深厚。

我和本书作者都认同编程是门手艺。而手艺人所最看重的,不是最后精彩绝伦的成果,而是成就这个结果的精雕细琢的过程。因为“授人以鱼,不如授之以渔”,所以我在写自己的《驯服烂代码》时,就刻意通过详述若干个带有 GitHub 微量代码提交的实例的编写和思考过程,来在编程操练中悟道。可喜的是,本书作者也看到了这一点,在本书最后两章中,提供了两个带有 GitHub 微量代码提交并能涵盖前面所讨论的主要测试原则的编程实例,来供读者体会作者用测试驱动开发方法开发Java新项目和用测试来驯服已有JavaScript烂代码的详细编写过程。这对读者正是 “授之以渔”。这意味着,在阅读本书前面讨论原则和模式的内容时,遇到不懂的地方也不要紧,在重现最后两章的带有详尽提交注释的微量代码提交的实例的编写过程中,就能体悟到前面所讨论的原则,并能从其中悟出更多的“道”。

重点突出的测试实践原则,富有特色的测试实践模式和微量提交的编程实例,令我对本书爱不释手,最终选择翻译本书,并在本书的启发下完成了我自己的拙作《驯服烂代码》。

伍斌

匠艺程序员,公益编程操练社区“bjdp.org北京设计模式学习组”创办者

前言

在过去的几十年里,精益(Lean)生产方式已经革命性地彻底改变了制造业。像全面质量管理(TQM)、准时生产(Just-In-Time)、约束理论(Theory of Constraints)和丰田生产方式(Toyota Production System)这样的框架已经大幅地改善了汽车质量和制造质量的总体状况,并造就了一个充满活力的竞争局面。各种敏捷软件开发方法都将精益生产的原则引入到软件产品开发的知识体系中,但这些原则需要改造,以适应软件开发这个不同于机械制造的领域。

像那些为了提升客户满意度和减少产品整个生命周期的维护成本,而将质量内建到产品中的想法,已经造就了测试驱动开发(TDD)和其他测试先行(test-first)及尽早测试(test-early)的方法的诞生。无论遵循哪种方法,都需要了解可测试的软件看起来是什么样子的,并掌握广泛的技术来成功地实现测试。笔者已经发现,在各种测试的调理方案中,上述原则和实践现状之间所存在的差距,往往是一个未被引起重视的失败根源。“使用测试驱动开发”这句话说起来容易,但是当面对一个项目时,许多开发人员都不知从何下手。

当向人们展示如何运用测试驱动开发或者尽早测试开发时,经常发现那些,其中的一个障碍就是编写测试的技术性细节。如果以数学函数的方式来执行一个方法,单纯地将输入转换成所期望的输出,那是没有问题的。但是很多软件都有副作用、行为特征或上下文约束,这些都使得对其进行测试并不是那么容易的。

本书源自下面这样经常性的需求,即要为开发人员展示如何针对那些曾经让他们挠头的具体场景进行测试。当这种需求来临时,我们总是会坐下来,为那些惹麻烦的代码编写一个单元测试,使开发人员能够拥有一个新的工具。

本书内容

本书讲述了如何对所有的软件进行轻松的例行测试。主要侧重于单元测试,但是其中的许多技术也同样适用于较高层次的测试(higher-level test)。本书将为读者提供一些工具——一些实现模式(implementation pattern),这些工具几乎可以测试任何代码,用它们还能够识别出代码什么时候需要改成可测试的。

本书目标

本书将帮助读者:

了解如何对所有的代码进行轻松的单元测试;

提高软件设计的可测试性;

针对代码找到适用的测试替代方案;

以和应用程序的生长相适应的方式来编写测试。

为了达到这些目标,本书将提供:

20多个测试代码的具体技术和许多例子;

在各种场景下使用正确的测试技术的指南;

一种能帮助读者更加专注地进行单元测试的方法。

本书读者

本书主要针对那些希望提升自己在代码层面的测试技能以增强代码质量的、专业的软件开发人员和在测试领域的软件开发人员。本书尤其对那些测试驱动开发(TDD)和尽早测试的实践者们特别有用,可以帮助他们从一开始就确保其代码的正确性。本书中的许多技术也同样适用于集成测试和系统测试。

读者背景

本书假定读者具有以下特点。

熟练掌握面向对象的编程语言,能够阅读和理解 Java、C++和其他语言的样例代码,并能将从中学到的知识运用到自己所运用的编程语言中。

熟悉代码层面测试的概念和像JUnit这样的xUnit测试框架的工具。

了解或可以查阅到有关设计模式和重构的信息。

本书章节

第一部分(第1~5章)涵盖了能成功指导测试的原则和实践。第1章将本书所讨论的方法放到工程的背景下,讨论了工程、匠艺(craftsmanship)和首次优质[1](first-time quality)的一般概念,以及一些针对软件的具体问题。第2章探讨了意图(intent)的作用。第3章描述了一种能帮助读者更加专注地工作的测试方法。第4章讨论了设计和可测试性之间的相互作用,其中包括了一些能够帮助拓展测试工作的想法。第5章介绍了一些能够用来指导做出测试决策的测试原则。

第二部分(第6~13章)详细介绍了测试的实现模式。首先,第6章介绍了bootstrapping测试,并讨论了相关技术的基本类别,而第7~12章则对第6章介绍的内容进行了详细的阐述。同时在第9章对意图这个概念进行了更加深入的研究。第13章则通过引入一些能确定性地重现竞态条件[2](race condition)的技术,在技术上更深入地探究了许多人认为不可能办到的事情。

第三部分(第14~15章)详细描述了如何把本书前面所讨论的原则和技术,运用到两个真实工作中的例子上。第14章探讨了使用测试驱动开发来从头创建一个Java应用,展示了如何开始以及如何将上述技术运用到一个严格定义类型的语言上。第15章选择了一个未写测试代码的开源JavaScript jQuery插件,来为其添加测试代码,展示了驯服用动态语言编写的遗留代码的方法。在描述过程中,这两个例子都包含了详细的GitHub的代码提交历史信息,以供参考。

同道前辈

人类所有的进步都是建立在他人先前努力的基础之上的。本书所讨论的内容正是在过去15年计算机发展的影响下成长起来的。下面的列表并不详尽,希望列表中有遗漏或宣传得不够到位的地方不会冒犯这些前辈。

敏捷宣言(Agile Manifesto)的推动者们和签署者们。

早期敏捷开发方法的先驱们,如极限编程(eXtreme Programming)的先驱Kent Beck。

提出设计模式的“四巨头”[3](Gang of Four)和提出重构的Martin Fowler。

软件匠艺运动和人称“Bob大叔”的Robert C.Martin。

最近Michael Feathers和Gerard Meszaros在软件测试领域所做出的开创性的工作。

很有幸曾与上述杰出前辈的一些原团队的同事们一起共事过。

[1].首次优质,是指第一次就把产品质量做好的理念,以消除后来检查产品质量缺陷的成本和时间。——译者注

[2].竞态条件,指一种电子或软件系统的行为,该行为的输出依赖于其他不可控事件的发生顺序或发生时间(引自wikipedia.org)。——译者注

[3].四巨头,指《设计模式》(Design Patterns)一书的四位合著者Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides。——译者注

致谢

所有的作者都会说,如果没有生活伴侣的支持,他们是不可能把书写完的。对于这一点,直到自己亲身经历写书的过程后,才真正地体会到其中的滋味。如果没有妻子珍妮的持续支持和鼓励,没有她对写书所费时间的耐心,没有她每日所说的“我来刷盘子,你去写书吧。”这样的话语,真的没法完成这本书的写作。

感谢Greg Doench对于头次写书的作者所表现出的耐心,以及他在本书的编辑过程中所提供的经验丰富的指导。Zee Spencer、Eric E.Meyer和Jerry Eshbaugh针对本书的审阅反馈帮助改进了本书的内容,并使其更加专注。希望本书没有辜负他们所提供的反馈。

开源软件项目jQuery Timepicker Addon插件是本书第15章中使用的样例的主体。该插件作者Trent Richardson一直都对把这个开源项目的代码纳入测试的保护之下这件事抱有极大的热心和支持。他接受了迄今为止的所有pull requests[1]。在撰写本书之时,该项目的第一个带有测试的版本上线了。

多年来,笔者曾辅导和管理过多个团队。一个人只有在去教会别人的过程中,才能让自己真正明白一些东西。感谢这些团队让笔者得以成长。

有了《Dr.Dobbs’Journal》和《Software Development》杂志的多位作者的激发,笔者才产生了有关尽早测试方法一些早期的想法。Eric E.Meyer、Jean-Pierre Lejacq和 Ken Faw 展示了有关 TDD 的一些训练有素的方法。笔者的机会,许多都是Christopher Beale 提供的,或者和他一起创造的。我们的事业交织在一起,这包括我们和Joe Dionise在一起所做的工作。许多早期的设计和架构经验,都是在他们的指导下形成的。

最后,两位教授在不知不觉中对笔者产生了很大的影响,这种影响在本书中达到了极致。20世纪90年代初,曾在密歇根大学任教的Lee教授,向笔者展示了计算机科学可以不仅仅是一种爱好。在硕士课程的第一堂课中,奥克兰大学的 Janusz Laski教授介绍了形式化验证(formal verification)和静态与动态分析方法,对理解和宣传那些支持软件开发过程的工具很有帮助。

[1].pull request指的是程序员在GitHub修改代码后所发出的用来通知其他程序员有关该代码变动的请求。GitHub是一种基于Web的使用代码版本控制系统Git的软件开发项目托管服务,用来管理源代码的版本变更。——译者注

第一部分 测试的原则和实践

测试,尤其是用代码编写的自动化测试,贯穿软件工程的始终。无论是通过测试驱动开发、持续集成、行为驱动开发、持续交付、验收测试驱动开发、实例化需求(specification by example)、集成测试、系统测试,还是单元测试,每一个参与基于软件的产品开发的人,都会有机会编写自动化测试。敏捷、精益(lean)[1]和软件匠艺运动都赞成使用高水平的测试,来支持快速开发和高品质的产品。

软件工程领域的思想领袖们,把基于代码的测试作为专业开发人员的保留节目来加以推动。而人称“Bob 大叔的”Martin[CC08,CC11]则把它称为软件匠艺。Gerard Meszaros[xTP]整合了与其相关的词汇。Steve Freeman和Nat Pryce[GOOS]描述了如何使用健康的测试剂量来培育软件产品的开发。Michael Feathers[WEwLC]为大家展示了如何通过测试来拯救旧代码,他甚至把遗留代码(legacy code)定义为没有对其编写测试的代码。Kent Beck[XPE]等人阐释了如何通过使用包括测试驱动开发方法在内的手段,来将编程发挥到极限。

上述杰出人物中的每一位,都描述了在软件开发中使用测试的一个重要部分。他们所撰写的每一本技术书籍,都使用了大量的示例来帮助大家领会他们各具特色的教导。

然而,当笔者用测试的调理方案辅导了多个团队之后,却一次次地发现阻碍采用测试的障碍,既不是对测试流程缺乏了解,也不是对相关概念的误解,或者词汇的缺失,更不是对测试实践的价值的怀疑。虽然所有这些障碍都因时因人而异,但是一个最常见的、一直没有很好地加以面对的障碍,就是没有充分地理解测试代码的机制和编写可测试代码的机制。

虽然本书专注于有关编程测试和可测试性方面的多种机制——各种实现模式,但是如果能带着对这些技术背后的概念框架的理解来阅读本书,那么就可以更加巧妙地运用这些技术。本书第一部分探讨了工程和匠艺之间的关系,并讨论了测试在匠艺中所起的作用。随后这一部分内容又展示了如何分别为既有代码和未写代码编写测试,如何更好地理解代码意图,可测试性是什么样子的,以及在编写测试时应该记住的指导原则。

第1章 工程、匠艺和首次优质

我们这个行业在很大程度上,把诸如软件开发人员、计算机程序员、编程牛人[2]和软件工程师这些头衔当作同义词而混为一谈。然而,由于“工程师”一词在制度上的严格规定,导致“软件工程师”一词在有些地方是不能使用的。软件这门学问一直以来都渴望成为一门工程学科,跻身于比较传统的工程学科(如土木、机械、电气和航空航天)行列。这一点在最近这些年里开始得到人们的承认,但是这期间还是伴随着相当数量的有关执照、认证和所需最小知识体系这些问题的争论。

与此同时,软件匠艺运动一直在稳步发展。伴随着在世界各地纷纷出现的软件匠艺组织和像CodeMash[3]和Global Day of Code Retreat[4]这样的活动的举办,越来越多的开发人员希望专注于提升自己的编程技艺。

1.1 工程与匠艺

有一件事可以用来区分软件工程与其他工程学科,那就是软件开发人员需要经常和全面地实践软件构建过程的各个环节;而来自其他工程学科的工程师们,一般只对和他们的领域相关的构建过程感兴趣,并拥有其技能,但是他们很少需要将其作为一项每日必做的活动。

汽车工程师可能会将时间花费在装配线上,但他们不会经常呆在那里。土木工程师可以监督和检查桥梁或建筑物的建造过程,但他们很少花时间去铆铆钉、浇筑混凝土或者穿挂电缆。与软件工程师这样的全程投入的职位最接近的,恐怕也就是极少数的几个身兼航空工程师的试飞员了,因为他们会参与自己将要试飞的飞机的设计、施工、检查和验证。

当在软件领域出现了这种复兴运动[5]后,其结果就是我们往往会淡化手艺(craft)、工程和创作之间的界限。另外,在软件开发中,还要去参与了解问题领域(problem domain)的内容,这就难怪会在工作中把所应关注的问题与其他问题纠缠在一起了。但讨论这些的用意何在呢?

专业的软件从业者,会在多个层次上进行编码、设计、架构、测试、度量和分析。这其中的一些内容,很明显就是工程。进行设计和由他人进行验证也很显然就是工程。但不管怎样切分,编码的行为,就是匠艺的行为,就是做出来的行为。当我们的手指在键盘上敲击的同时,我们或许是在并发地做一些“工程”,但这还是在做一门手艺。

1.2 匠艺在首次优质中的作用

匠艺蕴含着技巧。在编码中所使用的技巧,决定着编码的结果。无论对软件做了多少架构、设计或构思的工作,一个差劲的实现就能够破坏掉所有这一切。

作为传统,我们过去会依赖这样的验证,即通过自己的注意力或一个质量保证小组来手工确保软件行为的正确性。一位开发人员曾对笔者说:“我先编写代码,然后再训练它做正确的事情。”虽然他那种最终实现正确性的用意值得称颂,但是此话也表露出他对最初就把产品做好缺乏意愿和关注。当他花了1周时间在一个复杂系统中修改了一些核心代码后,令人哭笑不得的事情发生了,他又要求花3周时间来测试修改的结果。

精益制造是运作在将优质构建到产品中的原则之下的。返工是一种形式的浪费,需要从系统中消除[6]。而先编写软件以待将来发现bug时再重写或修补,就是返工。

测试那种本应正确的软件,就可以被看作是一种浪费。由于尚未找到创建没有缺陷的软件的方法,所以在编程之后进行测试在一定程度上是必需的。然而,当软件缺陷被发现、修复和重测时,对该软件所做的多于一次的测试显然是低效的。

另一种形式的浪费是库存,可以将其视为机会成本[7]。在修复 bug 上所花费的时间,也正是该软件中那些被正确编写的部分无法交付给客户或提供商业价值的时间。这个时间同时也是开发人员原本可以做包括职业发展在内的其他有价值的行为的时间。

提升工作中的匠艺水平,能为个人和公司带来成倍的回报。它能令人用更少的时间和更少的浪费,来交付更多的价值,并带来更多的个人满足感。

关于方法的一点看法

首先需要事先声明,笔者是软件开发中敏捷和精益方法的热衷者。然而,笔者也坚信,世界上不存在一个或一组方法,能够适合每个团队、公司、项目或产品的情况。笔者从事过同时使用敏捷和瀑布开发方法的相当标准的软件项目,也把敏捷开发过程嵌入到瀑布式开发过程中,甚至在很大程度上,把敏捷技术运用到涉及实时控制和有关人身安全的产品上,还曾经雄心勃勃地以整个组织转型为目标,把敏捷和精益方法运用到前沿的Web开发上。

无论使用哪种方法,项目中可能都已经有了某种形式的自动化测试,哪怕就是为了省却自己去按下按钮的麻烦。而更有可能的是,为了频繁地运行单元测试、集成测试、系统测试、组件测试、功能测试和其他形式的测试,项目已经拥有了某种形式的持续集成系统。

只要有测试自动化的地方,就会有人编写代码来测试其他的代码。对于这种情况,本书所讨论的技术将会很有用。对于单元测试或隔离测试(isolation testing),本书所讨论的几乎所有技术都适用。而对于更大范围的测试,或许可以选择来运用的技术就会少很多。一些技术只适合于某些编程语言,而其他技术则可被用于几乎任何编程语言或编程范型(programming paradigm)。

笔者会提及一些敏捷软件开发中常见的测试方法。即使有些方法不合某些人的开发口味,也不要让这一点成为去尝试它们的拦路虎。无论是实践测试驱动开发、测试先行、尽早测试或者测试后行(test after),都仍然需要驯服被测代码。祝读者有一个快乐的测试!

1.3 支持软件匠艺的实践

现在问题变成了这个样子:如何才能不断地提升软件从一开始就能被正确编写的可能性?首先,让计算机做它们所擅长的事情,即那些对人类来说,耗时易错的、需要死记硬背的、机械的行为和详细的分析工作。而利用自动化代码质量工具的不同方式,会导致有关软件卫生(software hygiene)的不同分类[8]

风格(style):空白字符、大括号、缩进等。

语法(syntax):编译、静态分析。

简单性(simplicity):圈复杂度、耦合、YAGNI[9]

解决方案(solution):运行时、正确性、是否能工作、TDD、BDD。

努力的伸缩性(scaling the effort):持续集成、持续交付。

集成开发环境(Integrated Development Environment,IDE)[10]能让我们轻松地拥有从“风格”到“解决方案”的丰富功能。代码补全(code completion)、语法加亮(syntax highlighting)和项目与文件的组织与管理这些仅仅是最基本的,重构支持(refactoring support)和其他工具集成才是最诱人的。如果能够办到的话,可以在IDE中配置所有的工具,使它们能够提供直接的实时反馈。编写代码和获得反馈之间所消耗的时间越短,就越接近首次优质的目标。

不要让自己担心大括号的位置和空白字符的数量和类型[11],可以配置IDE或构建工具来做这些有关代码格式的事情。理想情况下,IDE会做这些事情,使得代码格式的设置能够立即生效。而随时能在本地运行的构建工具也会为构建目标做这些事情。

可以用静态分析工具进一步增强对代码“语法”和“简单性”这两个分类的保护。像下面这些工具,如针对Java语言的Checkstyle,针对JavaScript语言的jshint或jslint,针对Perl语言的perlcritic,针对C和C++语言的lint,或者针对其他所选用语言的类似的工具,都可以用来帮助检查有关代码风格方面的问题。而像 PMD[12]或 FindBugs这样针对 Java 语言的更加高级的工具,则在代码格式规范和风格检查之外更进了一步,它们直接检查诸如未使用的和未初始化的变量、框架特定的约定(framework-specific conventions)和复杂性度量等各个方面。有些工具甚至可以进行扩展。特别是PMD这个工具,具有非常灵活的功能,它能基于使用抽象语法树的XPath表达式来定义规则。PMD还有一个称为CPD的模块,即复制-粘贴探测器(copy–paste detector),能用来加亮那些复制而来的代码,以便进行重构并复用。

代码覆盖率工具能通过数值和图形的方式显示代码中哪些部分已经被执行,来指导测试并帮助开发人员找到正确的解决方案。本书后面的部分将更加密切地关注代码覆盖率。

代码生成工具能够根据源代码或其领域特定语言[13](Domain-Specific Language,DSL)来创建成千上万行代码,将可能帮助完成“简单性”和“解决方案”这两个分类的工作。目前已经有可以生成网络服务接口、数据库访问代码和更多其他代码的工具了,尽管在许多情况下,这样生成的代码都具有代码腐臭[14](code smell),因而丧失了复用的机会,以及完成“简单性”分类工作的机会。在某些情况下,我们甚至能够找到帮助生成测试代码的工具。如果能够信任代码生产器,那么就可以跳过对所生成的代码进行测试的步骤,从而节省同时在两条战线作战所耗费的时间和精力。

对于工具要讨论的最后一点是,持续集成系统除了能够针对代码进行构建和测试工作以外,还能运行所有这些工具,以使代码能够独立于本地开发环境,从而帮助完成“伸缩性”的工作。持续集成所构建的结果,应该可以通过Web、监视器和电子邮件来获取到。许多团队已经将他们失败的持续集成的结果与警笛或警灯绑定在一起,使其更加明显。为取得最佳效果,持续集成的构建应该在几分钟之内就能报告结果。持续集成提供了另一种提供快速反馈的形式,用牺牲一些响应时间作为代价来换取对代码所做出的更加全面的评估。

测试

在所有能够利用的有助于提升匠艺的实践中,令人最为获益的是测试。有人可能会说:“我们已经对软件进行了多年的测试,但还是能够发现许多 bug。”取得进步的关键其实就在于此。

在一个精益系统中,我们要做的是努力阻止缺陷,而不是捕获缺陷。而传统以来,软件测试一直是作为捕获缺陷的一种方法。所以,我们一直沿用着一个低效的过程这一点,不应该令我们感到惊讶。越早地进行测试,就能越早地捕获缺陷。

这是不是在说我们还是仅仅在捕获缺陷?如果是在编写生产代码后才编写测试,那么就是仅仅在捕获缺陷。如果能弥合创建缺陷和捕获缺陷这两者之间的间隙,那么代码在这两者之间所发生的变化就会更小,那么就更有可能让刚刚创建的代码的上下文在头脑中记忆犹新。如果在编写代码后立即就编写测试,那么测试就能马上验证有关上述代码的概念和假设,并保证开发走在正确的方向上。

然而实际上,在bug发生之前,我们就有机会捕获到它们。测试先行和测试驱动的开发方法,都是在编写代码之前先编写测试代码。当使用测试先行的方法时,可以用自动化和可执行的形式来捕获设计意图。此时,专注于用阻止缺陷而不是创建缺陷的方法来编写代码并令其能够工作。这里所编写的测试,就是对下一步的意图所进行的持久化的加固,它不仅能帮助我们把事情做好,还能帮助我们把事情做对。记住,一件错误的事情,不管做得多好,它都仍然是一个bug。

在上述模型中,测试驱动开发(Test-Driven Development,TDD)是测试先行方法的一个子集。测试先行允许我们在编写生产代码之前,根据自己的想法,编写出又多又全的测试代码。虽然测试先行把质量工作放在了首位,但它在推测要做的事情和所要采取的形式方面,也给了我们更大的自由度。一旦所设计的解决方案比所需的还通用,那么就不仅要面临在该方案的创建上花费更多时间的风险,还要面临背上更沉重维护负担的风险,这是一个需要一直承担的无限期的成本。此外,若前面编写了太多的测试,一旦发现解决问题的更好方法,就会增加修改的成本。因为有这些原因,TDD为编写软件带来了更加有纪律性和精益性的方法。这种方法仅仅当需要时才编写恰好所需的代码。正是对以前不足够的预见和不完整的理解的一个个发现,指引着编程的方向。

无论编写测试的时间与编写生产代码的时间这两者之间处于怎样的关系,本书所提出的原则,都将会有助于进行有效的测试。但是,测试进行得越早,本书所提供的帮助才会越大。敏捷开发和测试的其中一条准则是自动化测试三角(Automated Testing Triangle,见图 1-1)。传统上,我们一直专注于系统级别的测试,而这种测试不是因为验证得过于深入而趋向于脆弱,就是因为要达到便于维护的目的而趋向于粗略。虽然我们永远都无法取代系统级别的测试,但是可以将一部分测试转移到级别低一些的单元测试里。

笔者听说有人反对在上图的底部进行大量的单元测试,因为它们很脆弱、难以维护。然而编写得当的单元测试的特点恰恰与上面所说的相反。本书中所讨论的原则和技术将有助于理解单元测试变得难缠的原因,以及如何避免这种情况发生。

1.4 在代码检查器的约束下进行单元测试

在前面笔者曾经提到,应该使用工具来将匠艺所关注的内容尽可能多地进行自动化。如果用测试来驱动开发的质量,那么也需要将良好的匠艺运用到测试上。我们的工具不仅应该检查应用程序的代码,还应该以几乎同样的标准检查测试代码。笔者在这里说“几乎”,是因为一些针对生产代码的用启发方式对代码进行复用的方法,需要在测试代码中放得宽松一些。例如,在两个不同的测试里使用重复的字符串值,能够使这些测试彼此相互独立,尽管使用不同的字符串会起到更好的效果。同样,虽然针对复用的重构能够为测试代码带来好处,但是不应该将测试重构得把设置(setup)、执行(execution)和验证(verification)这三者[15]彼此之间的界限给搞模糊了。

将静态检查器的约束应用到单元测试时,需要缩小该约束可以应用的解决方案的范围。例如,假设我们想在嵌套类中使用一些常量值,同时使用一系列默认的 PMD规则,那么我们就会被迫将许多上述常量值被移到测试方法之外。可以用许多方法来解决这个问题,但如果这个问题成为了团队的负担,且没有带来多少价值,那么就可以将正常规则的一个子集或者甚至是一个等价的定制集合,应用到测试代码上。

不管怎样,用与应用程序代码相同的标准对待测试代码,会在将来很长的时间里得到回报。如果读者觉得本书中一些例子的实现形式得看起来很奇怪,那么可以试着想象一下静态检查器打开时实现这些代码会出现的情况,或许就能明白原因了。

1.5 针对覆盖率的单元测试

代码覆盖率工具为编写、雕琢和维护测试提供了极好的帮助。这些工具用图形化的方式显示了哪些生产代码在测试中得到了运行,从而让我们得知测试是否命中了目标以及哪些生产代码还未被命中。

代码覆盖率所提供的并不是一个有关生产代码如何被执行的详尽情况。代码的许多构造会以不能被静态确定的方式分支到多个代码路径上。各种编程语言中的一些最强大的功能(那些提供最有用的抽象的功能)会以各种让静态分析无从探知的方式来工作。当使用代码覆盖率这个机制时,虽然覆盖率工具能够显示出哪些生产代码已经被执行,但是没有办法预先知道会存在多少排列组合,来从代码运行中计算代码覆盖的程度。

数据驱动执行(data-driven execution)通常以从数组或表中进行查询的方式来替换链式的条件语句(chained conditional)。在代码清单1-1中,覆盖率工具虽然能给出针对这个情况的语句覆盖率的准确评估,但是无法给出表中那些转换点所代表的每个条件,是否像用if...else语句链实现那样被覆盖。假如rateComment数组中的那些函数是在其他地方定义的,而且有被复用的可能性,那么覆盖率工具就会漏掉更多的情况。

代码清单1-1 一个展示代码覆盖率盲点的用JavaScript编写的数据驱动执行的例子

function commentOnInterestRate(rate) {

var rateComment =[

[-10.0,function() { throw "I’m ruined!!!"; }],

[0.0,

function() { gripe("I hope this passes quickly."); }],

[3.0,function() { mumble("Hope for low inflation."); }],

[6.0,function() { state("I can live with this."); }],

[10.0,

function() { proclaim("Retirement,here I come."); }],

[100.0,function() { throw "Jackpot!"; }]

];

for (var index = 0; index < rateComment.length; index++) {

var candidateComment = rateComment[index];

if (rate < candidateComment[0]) {

candidateComment[1]();

break;

}

}

}

动态分派(dynamic dispatch)提供了另一种机制,在这种机制下,代码的执行无法得到静态分析。看看下面这行简单的JavaScript代码:

someObject[method]();

这个变量method的值只有在运行时才能知道,这使得在一般情况下,覆盖率工具不可能确定代码可用路径的数目。在JavaScript中,方法的数目会随着代码运行的过程发生改变,所以即使看到一个对象的已知方法,也不能凭借它来计算覆盖率。这个问题并不限于动态语言。即使是像C++和Java这样的标准的静态类型语言,都有动态分派功能,其中,C++可以通过虚函数和指向成员的指针引用(pointer-to-member reference)获得动态分派功能,Java可以通过反射(reflection)来获得。

类似的其他情况会发生得更加自然一些,可以称为边界情况的语义处理(semantically handled edge cases)。在这些情况中,编程语言或运行环境会把一些异常条件自动翻译为一些变体,系统在处理这些变体时,会采用与正常执行所不同的方法。Java语言的unchecked异常和那些不必在方法签名中声明的异常,都会发生这种情况。这还包括在试图使用一个 null 引用时所抛出的最臭名昭著的、可怕的 Null PointerException(空指针异常)。而对于被零除(divide-by-zero)这样的错误,不同的语言则有不同的处理方式,这些方式包括整个应用崩溃、抛出可捕获的异常和从计算中返回NaN[16]

另外,代码覆盖率也是具有欺骗性的。覆盖率仅显示了被执行的代码,而不是那些被验证过的代码。覆盖率的有用之处仅仅和驱动它的那些测试相同。即使是富有善意的开发人员,在覆盖率报告面前也会变得沾沾自喜。下面的这些轶事,是笔者以前带过的一些团队无意中所犯的错误,通过它们,可以想象到滥用覆盖率所造成的人为的后果。

开发人员在编写了测试的setup和execution部分之后,就开始有些分心了,因为一会儿就要回家过周末了。等到了周一上午,在他运行代码包时,因为失去了过周末之前的那些背景信息的提醒,他在验证实现了全覆盖后,就把代码提交了。事后对代码进行检查发现,他所提交的测试虽然全部执行了被测代码,但是却没有包含任何断言。该测试实现了代码覆盖并运行通过,但是代码包含了bug。

开发人员编写了一个Web控制器,用来控制这个应用程序内两个页面之间的交换。由于不知道一个特定条件的目标页面,他使用了空字符串作为占位符,并且编写了一个测试来验证该占位符按照预期返回。该测试运用通过且显示全覆盖。两个月之后,一位用户报告说,应用程序会在一个晦涩的条件组合下返回主页面。根本原因分析显示,那个空字符串的占位符即使在正确的页面被定义后,也未被替换。那个空字符串会被拼接到应用程序的域和上下文路径后,重定向到主页面。

‰ 开发人员最近发现了 mock[17]的价值,并在 mock 魔力的吸引下编写了一个测试。他在不经意间mock了被测代码,并让测试运行通过。而其他测试偶然间使用了该被测代码,结果导致该代码被计入了覆盖率的统计。该系统其实没有达到全覆盖。后来为了有意义地增加覆盖率而对测试进行检查,结果发现了这个过失,并编写了一个测试去执行被测代码,而不是仅运行mock。

代码覆盖率是指南,但不是目标。覆盖率有助于编写正确的测试来执行代码的语法运行路径。人的大脑还是需要多用一用的。同样,人们所编写的测试的质量,依赖于这些人在编写测试时所投入的技能和注意力。覆盖率对于检查意外或故意造成的低劣的测试无能为力。

请注意在覆盖率这一点上,前面并没有谈及要使用哪种覆盖率指标。几乎所有人都认为要使用语句或行数覆盖率。几乎所有的覆盖率工具都提供语句覆盖率的统计,但这仅仅是起点,就好比一进赌场就能得到的那些入场筹码而已。不幸的是,许多人就到此止步了,有时甚至用更加薄弱的类和方法/函数覆盖率来作为对语句覆盖率的补充。而笔者则更喜欢最低限度地使用分支和条件覆盖率[18]作为补充。一些工具包含分支覆盖率功能。可悲的是,没什么工具能包含条件及更多覆盖率的功能。一些额外的指标包括循环覆盖率(每个循环必须执行0次、1次和多次)和像def-use链[19]这样的数据路径指标[LAS83,KOR85]。在 Java 语言中,开源工具 CodeCover (http://codecover.org)和Atlassian公司的商业工具Clover在这方面做得不错。Perl语言的Devel::Cover工具也处理了多个指标。PMD会对UR、DU和DD异常[20]的数据流分析发出错误和警告信息,尽管这些信息需要再改进一下。

笔者似乎十分喜爱那些高可用、高可靠和注重安全性的软件。笔者以前带领团队开发过紧急响应软件、实时机器人控制(有时需要与非人眼安全的激光技术相结合)和高利用率的构建和测试系统,对于这些项目来说,停机时间意味着真实的业务遭受延误和损失。笔者还曾带过一些项目,在这些项目中,100%的语句、分支、条件和循环覆盖率仅仅被当作全面单元测试的一个里程碑。相对于应用到这些系统上的许多其他级别的测试,那时除了仅从单元测试中统计覆盖率以外,还要仅从一个类的测试中来统计该类的测试覆盖率。那些其他类偶然使用的覆盖率不会被纳入统计。

一般情况下,笔者发现当语句覆盖率在50%左右时,优质代码的回报就开始显现出来。当这个数达到80%时,回报就会变得有意义。如果达到了100%这个里程碑,我们就能获得显著的回报,但是这样做的成本取决于开发人员在测试和编写可测试的、低复杂度的、松耦合的代码这些方面的技能[21]。先暂且不管决定回报的成本需要依赖于特定的问题领域这件事,现在大多数团队还没有对覆盖率准确地做出评估以便进行权衡的经验。

通常情况下,团队会基于不值得做 100%覆盖率这样的观点,来选择一个小于100%的数作为目标覆盖率。一般来说,有观点认为,不宜做测试的情况分为两种:琐碎的和困难的。反对编写困难的测试的观点,则集中在算法复杂项或错误路径这两个方面。

琐碎的测试包括对像getter和 setter 这样的简单方法的测试。确实,测试它们很无聊,但是对它们进行测试花不了多少时间,而且这样一来,就永远不必怀疑覆盖率的缺口会是由这些琐碎的测试的疏漏而造成的了。

算法复杂的代码最有可能是系统的独家秘方的心脏——就是靠这个独家秘方把自己的软件与其他公司的软件区分开来。这听起来好像是需要测试的东西。如果实现[22]的复杂性阻碍了测试,那么代码就可能需要进行设计的改进,而这个改进可以通过可测试性的需求来驱动。

错误路径的测试是用来验证系统中那些最有可能令客户心烦的部分的。很少能在有关软件的线上的评论中看到赞誉系统没有崩溃并优雅地处理了错误这样的内容。发生了崩溃、丢失数据和其他严重失效的软件,会得到非常差劲和公开的评论。抱着永远乐观的心态,开发人员希望并几乎假设那些错误路径很少会被走到,但现实是,这个世界是十分不完美的,所以这些错误路径肯定会被走到。测试错误路径就是在逆市中对客户的良好意愿的投资。

最终,自己要对业务所需的测试程度做出决策。建议在做决策时,要从一个熟练工匠(即能够实现业务所要求的任何覆盖率的人)的角度来考虑,而不要因为看起来很难就避免高覆盖率。本书的目的就是要用模式、原则和技术把读者武装起来,使读者能站到熟练工匠的角度上做决策。

相关图书

现代软件测试技术之美
现代软件测试技术之美
渗透测试技术
渗透测试技术
JUnit实战(第3版)
JUnit实战(第3版)
深入理解软件性能——一种动态视角
深入理解软件性能——一种动态视角
云原生测试实战
云原生测试实战
Android自动化测试实战:Python+Appium +unittest
Android自动化测试实战:Python+Appium +unittest

相关文章

相关课程