欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

从三本书规劝人单元测试

最编程 2024-02-24 16:40:46
...

《人月神话》

《人月神话》(1974年首次出版)中部分内容非常过时,会让人根本不知道他在讲什么(比如Ada语言(一门编程语言)、OS/360(一个操作系统)、Model 75(一个计算机体系?)、TESTRAN(一个调试程序)等等),有些内容却会让你大喊“太对了,就是这样!”,“原来这么早就有结论了吗?!”等等。不过这种强烈的对比,也体现出那一部分,也就是会让你喊“太对了,就是这样!”的结论,在历史的长河中有着经久不衰的生命力。

人月神话在当下最经典的价值,是其论证了软件开发的主要困难是复杂度,而主要的复杂度来自于设计概念,次要的复杂度是实现概念。换句话说,最困难的事情是知道“该怎么做才正确” ,而不是“做出来”。而正是因为搞清楚该怎么做特别的困难,才导致软件开发的效率上不去、可靠性难以提升、软件越来越难理解。

image-20220219164005720.png

注意,《人月神话》的作者本着矫枉程序员过于乐观的心态和行事风格,行文和论点都有些悲观主义。不过大家不用太在意,这只是这次分享的开始,我们所开发的世界会一点一点美好起来的,本次分享也会明朗起来~

接下来,从软件的组成和特点出发,论证这个论点。再回顾我们现有使用的开发模型,展开更多的观点和思考。

软件的组成

我们可以分成四个层次,组件的选择、组件的概念、流程的设计、具体的编码。

  1. 组件的选择。比如我们有一个前端服务,两个后端服务,两个后端服务各有一个MySQL数据库,共用一个Redis。虽然这里已经涉及到了组件的概念,但是这里想强调的是组件的数量和外部依赖的选择(比如MySQL、Redis、Nginx等等)
  2. 组件的概念。比如两个后端服务各自有不同的功能。
  3. 流程的设计。比如两个后端服务怎么相互通信,某个服务怎么做鉴权等等。
  4. 具体的编码,if、for、func、return这些细节的东西。

这其中每一个部分都可能崩坏,而其中任何一环崩坏,都会导致最终软件的失败。顶层组成部分比较少见失败,一般崩溃的原因就是外部依赖过多、选择了不合适的外部意外、调用链过于复杂等原因。其他几个部分崩快的原因,我们顺着软件的固有特点一起看。

软件的固有特点

《人月神话》的作者认为软件有“无法规避的内在特性:复杂度、一致性、可变性和不可见性”。我个人对于认为这个翻译会导致一些误解,更好的翻译可能是“琐碎的细节、不存在通用理论、可变性、不可见性”。

不存在通用的理论

这一点比较好理解。比如物理、数学,这些学科的研究学者相信宇宙不是无序的,必然存在通用的理论。物理学家想在夸克中、或者统一场论中,找到某种通用理论。数学家通过证明,得出更加简单的公式来解释问题。

但是软件工程师得不到这样的安慰,因为软件的复杂度来自于不断变化、来来去去的人。因此软件的复杂度无法下降。

可变性

这很好理解,我们现在开发的软件就依旧在不断地变化,因为我们的开发模型是增量开发模型,而不是瀑布模型或者其他。同时变化的还有客户需求、运行环境、企业目标、部门结构等等,这些都会导致软件的变化。

然而新功能的增加,并不像在原有的积木上堆积木一样简单。一个原本设计良好的程序,很容易在功能的不断添加中崩坏,究其原因,是因为原本的概念设计,并不能满足之后的变化。而面对变化,如果只是在原有的概念上做加法,不对重新梳理概念,则会造成概念的混乱。而一个不清晰、不自洽的概念设计,恰恰是让软件变得复杂的根本原因。

这一特点,正对应着软件中各个组件的概念,对具体功能的变化,也对应着流程的设计

我们的程序在什么情况下因为变化而丑陋甚至崩坏? 停下来想一想。

不可见性

相比之下,建筑有建筑图,陆地和海洋有地图,电路有电路图。而软件没有任何空间上的特征。

(听众会有争议)我们常常使用的流程图、时序图、数据流图、状态流图等等的图来将我们的设计可视化。确实,这些图让我们可以更好地表达、研究我们的设计,但是设计本身的难度没有降低。

可能一开始的设计是一个清晰的,只有一个选择分支的线性流程。但是这个图并不可以真正反映程序。由于并发的存在,一个资源的状态转换并不那么简单。由此,我们需要真正的流程图来设计基本的逻辑,要使用时序图 / 状态流图来考虑并发,可能还需要数据流图来考虑数据的存储、获取、加工。对于数据库,我们有专门的**ERD图……**设计本身的复杂度并不因为这些图的发明而降低,这些图都只是软件的宏观而片面的描述,这些图只是为我们研究程序的设计提供了片面的表达工具,我们的设计依旧难以尽善尽美。

这也解释了,为什么有了这些设计,我们依旧可能将一个功能实现坏。首要原因是我们的设计不完美(就如上文所讲),不过还有一个次要原因,设计和编码之间还有gap。

image-20220219162549824.png

image-20220219163026024.png

这一特点,可能是最重要的一个特点,其对应着组件概念流程设计,还有流程设计与具体编码之间的GAP。

我们的程序什么时候因为设计的复杂性而丑陋甚至崩坏? 停下来想一想。

琐碎的细节

虽然程序的基本结构是非常简单的,只有循环、判断、计算、函数调用等等结构,然而在具体的编码上,我们堆砌的都是不同的内容,这些内容让程序的复杂度以非线性的速度增加。

随着复杂度的上升,列举所有的可能性变得困难、函数调用变得复杂、新功能的添加变得困难;软件越来越像一个黑盒。

这一部分,正对应着具体的编码

我们的程序什么时候因为具体的编码而丑陋甚至崩坏? 举例:比如一不小心写下的错误,if err != nil{ return nil};变量名起错;一个复杂的与或非等等。

image-20220219163822400.png

可以看到软件的固有特点,就是围绕着复杂度、组件、流程、编码来的。软件主要困难:复杂度和软件的组成、软件的特点有着内在的联系。

开发模型

没有困难的工作,只有勇敢的打工人。我们的开发模型是怎么应对软件的种种固有特点的。

瀑布和增量

曾经软件工程在开发模型上走过歧途,人们过于自信自己的理智,低估项目的复杂度,将瀑布模型视为高质量软件开发的唯一标准。

瀑布模型

推迟实现,严格依次执行需求分析、设计、实现、测试、回归、发布、部署。

对整个项目使用瀑布模型,这意味着我们必须从一开始就确定好所有的需求、然后做好所有的设计、全部实现后再进行测试。

20220219161311366.png

现在我们流行对于整个项目,我们使用增量模型

增量模型

每隔一段时间发布一个版本,在版本发布的时间间隔内做完整的需求设计、编码、测试、回归,最后的固定的时间节点发布多个新功能/修复。

对整个项目使用增量模型,意味着我们可以先做出一个只含有基本功能的程序,然后通过多次版本发布来不断完善功能,而每个版本都是一个可用的程序。

20220219161311185.png

我们回顾软件的固有特点,会发现这两种开发模型都不可能是完美,增量模型纵容了可变性,导致开发过程首尾不能相顾;瀑布模型忽视了不可见性,在复杂场景下我们几乎不可能从一开始就考虑周全。

因此我们倾向于在不同的场景中使用不同的开发模型,我们目前的模式是,对整个项目使用增量模型,随着客户和市场去不断发布新的版本,而对于单个新功能,我们大多数情况下是瀑布模型,先设计好,再去开发和测试;部分比较大的功能,我们可能会拆分成多个小需求,对每个小需求使用瀑布模型。

瀑布单个需求的开发

在对单个需求使用瀑布模型的过程中,《人月神话》提出了一种经验性的进度安排:1/3计划、1/6编码、1/4构建单元测试、1/4构建系统测试。可见在整个需求的开发过程中,最重要的是测试,应占用1/2的时间;次重要的是计划,占用1/3的时间。但是得在这里提醒,1/2的测试时间,是构建**自动化测试**+调试,而不是在本地运行起来、打个断点、看一下输入输出,这仅仅是调试。

image-20220219164139384.png

另一点,我们能不能跳过单元测试,直接集成测试?

——节选《人月神话》

(系统测试)使用经过调试的构件单元。尽管其并不是普遍的实际情况一一不过通常的看法是一一系统集成调试要求只能在每个部分都能正常运行之后开始。

实际工作中,存在着与上面看法不同的两种情况。一种是“合在一起尝试”的方法,这种方法似乎是基于这样的观点:除了构件单元上的bug之外,还存在系统 bug(如接口),将各个部分合拢得越早,系统bug就出现得越早。另一种观念则没有这么复杂:使用系统的各个部分进行相互测试,避免了大量测试辅助平台的搭建工作。这两种情况显然都是合理的,但经验显示,它们并不完全正确一一在系统测试中使用完好的、经过调试的构件,能比搭建测试平台和进行全面的构件单元测试节省更多的时间。

但即使如此,我们单个需求依旧很难高质量实现。单个需求依旧可能隐藏了大量的细节,导致我们无法一次性就面面俱到;或者即使实现了需求,但是实现地比较丑陋,不便于维护;或者实现了需求但是性能上不优。而这一切的根本原因是,设计太难了,我们不能一次就设计好。

但是,我们多设计几次,能不能设计好?

《重构》

《重构 改善既有代码的设计》这本书中提供了70多种重构的方法/准则/手法,我们不展开介绍重构的方法,我们介绍一下重构前前后后的一些事情。不过我们首先提出观点:通过严谨而敏捷的重构,我们可以在实现单个需求的过程运用快速迭代模型,又好又快地实现需求。 虽然刚刚说到,软件可以分为四个层次上的概念,重构也对应着这四个层次上的重构,但是我们这里主要讨论流程的设计具体的编码(最后两个层次)的重构。

软件工程希望建立完美的需求与设计,按照既有的规划编写标准划一的代码,这是结构的美。快速迭代颠覆全知全能的神话,用近乎刀砍斧劈的方法解决问题,在混沌的循环往复中实现需求,这是解构的美。而Martin Fowler(《重构》的作者)用敏捷而又严谨的方法论演绎了重构的美。

image-20220219164234864.png

事实上,我们永远无法在一开始就构建出完美的需求,设计出完美的程序,完美运用各种设计模式。

——节选《模型和XP》

在设计前期使用模式常常导致过度工程(over-engineering)

重构开创性地揭开了软件开发的后结构主义,得以让设计模式在快速迭代的互联网时代发光发亮。

重构在秩序和混乱、敏捷和质量中找到了一种维持平衡的方式。

什么是重构

——名词解释

在不改变代码外在行为的前提下,对现有代码的内部结构进行调整,以提高软件的可理解性,降低其修改成本。

——动词解释

在不改变代码外在行为的前提下,使用一系列重构准则(手法),调整其结构。

具体而言,我们从 前提、行为、目的 出发,理解重构

前提

重构不能改变软件的功能,也不能让用户感受到程序的变化。用户可能是客户、产品经理,也可能是程序员。

与重构的前提相似的是性能优化,和重构一样,性能优化不能改变程序的行为(除了执行速度),只会改变其内部结构。

行为

从表象来看,重构就是整理代码;学术地讲,重构是使用一系列的方法论,来整理代码(《重构》大部分篇幅都是在介绍相关的70余种方法论)。

只有一个重点:重构不是盲目而随性的,重构是有方法有策略的。

目的

重构的目的是为了让软件更加好理解,降低其修改成本。

重构改进软件设计/不容易腐败

这一目的,着重于软件的未来,而不是软件的当下。当我们只是为了短期目的,或者没有完全理解整体设计之前,就贸然修改代码,或者是两个还行的设计累加,造成了一个不好的设计……总之,各种各样的原因导致代码的结构慢慢流失,“软件慢慢腐烂”。而重构正是对这一过程的补救。

在《人月神话》中,对软件腐烂提供了更加严谨的数据:

——节选《人月神话》

  1. 缺陷修复总会以固定(20%~50%)的几率引入新的bug。
  2. (在操作系统中)模块的总数随着版本号的增加呈线性增长,但是随着单个模块修改而会影响到的模块数呈指数增长。

总结:软件维护是增加混乱度的过程,即使是最熟练的软件维护工作,也只是放缓了系统退化到非稳态的进程,以至于必须要重新进行设计。

重构使软件更加容易理解

这里的理解,有两层含义,一层是让现有软件更加容易让未来的人(包括自己)理解。另一层是在重构的过程中,自己会越来越理解这一块的设计。软件由细枝末节的细节组成。而我们总是不可能将所有的细节都牢记于心,强如HW,也总是说“让我看看代码再说”。

另外,小范围的重构能让我们看到更大范围内的“东西”

——节选《重构》

一开始我所做的重构都像这样停留在细枝末节上。随着代码渐趋整洁,我发现自己可以看到一些以前看不到的设计层面的东西。如果不对代码做这些修改,也许我永远看不见它们,因为我的聪明才智不足以在脑子里把这一切都想象出来。Ralph Johnson(《设计模式:可复用面向对象软件的基础》的作者之一)把这种「早期重构」描述为「擦掉窗户上的污垢,使你看得更远」。研究代码时我发现,重构把我带到更高的理解层次上。如果没有重构,我达不到这种层次。

和重构的目的对立的是性能优化:性能优化往往使代码较难理解,但是为了得到所需的性能不得不这么做。而重构为了让代码好理解,可能会让程序的运行速度下降,但是不能降低到用户可察觉的程度(这样就破坏了前提,性能也是用户可察觉的一部分)。

重构和性能优化有些对立,却又常常结伴出现。有时候为了执行一个优化,我们常常会先进行重构,再进行优化;或者让两者同时执行。

重构助你找到bugs

随着重构帮我们深入理解代码的行为,我们也更加清楚自己所做的一些假设,从这个角度来说,不找到臭虫都难。

就像Kent Beck(JUnit的作者,《敏捷编程解析》的作者)经常形容自己的一句话:

我不是个伟大的程序员;我只是个有些一些优秀习惯的好程序员而已。

重构助你提高编程速度

——节选《重构》

听起来有点违反直觉。当我谈到重构,人们很容易看出它能够提高质量。改善设计、提升可读性、减少错误,这些都是提高质量。但这难道不会降低开发速度吗?

我强烈相信:良好设计是快速软件开发的根本。事实上拥有良好设计才可能达成快速的开发。如果没有良好设计,或许某- -段时间内你的进展迅速,但恶劣的设计很快就让你的速度慢下来。你会把时间花在调试上面,无法添加新功能。修改时间愈来愈长,因为你必须花愈来愈多的时间去理解系统、寻找重复代码。随着你给最初程序打上一个又一个的补丁(patch) ,新特性需要更多代码才能实现。真是个恶性循环。

良好设计是维持软件开发速度的根本。重构可以帮助你更快速地开发软件,因为它阻止系统腐败变质,它甚至还可以提高设计质量。

《人月神话》和《重构》,矛和盾

image-20220219164258728.png

什么时候重构

一般而来,重构不需要进行排期,而应该随着开发随时进行。我们没道理为了重构而重构,而应该是因为想做别的什么事,而重构可以帮助我们把那些事做得更好。

从重构的目的中,发现我们一般会在以下几种情况进行重构:

  1. 刚刚写完代码的时候,对写下的代码不满意,所以进行重构。
  2. 编写新功能前,发现在现有的设计下编写新功能非常麻烦,所以先重构现有设计,再写新功能。
  3. Debug的时候,现在的设计不容易发现bug或者修复bug,则一并进行重构。
  4. 新人理解代码的时候,尝试进行重构,并将重构的结果交给熟悉项目的人Review。

什么时候不重构

  1. 如果现有的代码满是错误,根本不能正常运行,则应该直接重写。
  2. 如果项目已经临近最后期限,则应该避免重构。如果开发的最后,时间已经来不及了,则说明早该进行重构了。

——节选《重构》

Ward Cunningham(Wiki概念的发明者,设计模式和敏捷软件方法的先驱之一)对此有一个很好的看法。他把未完成的重构工作形容为「债务」。很多公司都需要借债来使自己更有效地运转。但是借债就得付利息,过于复杂的代码所造成的「维护和扩展的额外开销」就是利息。你可以承受一定程度的利息,但如果利息太高你就会被压垮。把债务管理好是很重要的,你应该随时通过重构来偿还一部分债务。

重构的前提

这一部分,作者原文意见讲的足够好了,因此我节选原文:

——节选《重构》

如果你想进行重构(refactoring),首要前提就是拥有一个可靠的测试环境。……我并不把这视为缺点。我发现,编写优良的测试程序,可以极大提高我的编程速度,即使不进行重构也一样如此。这让我很吃惊,也违反许多程序员的直觉,所以我有必要解释一下这个现象。

……

如果认真观察程序员把最多时间耗在哪里,你就会发现,编写代码其实只占非常小的一部分。有些时间用来决定下一步干什么,另一些时间花在设计上面,最多的时间则是用来调试(debug) 。我敢肯定每一位读者都还记得自己花在调试上面的无数个小时,无数次通宵达旦。每个程序员都能讲出「花一整天(甚至更多)时间只找出一只小小臭虫」的故事。修复错误通常是比较快的,但找出错误却是噩梦一场。当你修好一个错误,总是会有另一个错误出现,而且肯定要很久以后才会注意到它。彼时你又要花上大把时间去寻找它。

……

那时候我还着迷于增量式开发(incremntaldevelopment),所以我尝试在结束每次增量时,为每个 class添加测试。……做这些测试还是很烦人,因为每个测试都把结果输出到控制台 (conole),而我必须逐一检查它们。我是个很懒的人,我情愿当下努力工作以免除日后的工作。我意识到我其实完全不必自己盯着屏幕校验测试所得信息是否正确,我大可让计算机来帮我做这件事。我需要做的就是把我所期望的输出放进测试代码中,然后做一个比较就行了。于是我可以舒服地执行每个class 的测试函数,如果一切都没问题,屏幕上就只出现一个"OK"。现在,这些 classes 都变成「自我测试」了。

确保所有测试都完全自动化,让它们检查自己的测试结果。

……

注意到这一点后,我对测试的积极性更高了。我不再等待每次增量结束,只要写好一点功能,我就立即添加测试。每天我都会添加一些新功能,同时也添加相应的测试。那些日子里,我很少花一分钟以上的时间在调试上面。

……

当然,说服别人也这么做,并不容易。编写测试程序,意味要写很多额外代码。除非你确切体验到这种方法对编程速度的提升,否则自我测试就显不出它的意义。多人根本没学过如何编写测试程序,甚至根本没考虑过测试,这对于编写自我测试代码也很不利。如果需要手动运行测试,那更是令人烦闷欲呕;但如果可以自动运行,编写测试代码就真的很有趣。

《代码整洁之道》

我们讲到了,重构的目的包括让代码更容易理解,这恰好是《代码整洁之道》的主要内容。《代码整洁之道》以Tips的形式,阐述了如果让命名、函数、注释、对象、模块、错误处理等方方面面变得整洁清晰。这里无法罗列这些Tips,只能泛泛介绍一下,为什么要整洁,何为整洁。另外,《代码整洁之道》也提到了单元测试(这已经全文第三次提到单元测试了),我们不妨也看一看这本书如何教我们写测试。

书的PDF

为什么要整洁

20220219161312112.png

沼泽

一言以蔽之:糟糕的代码让我们效率低下,而改进糟糕代码最好的时机就是当下。

这里的观点,与上面讲到的差不多。我们把重点放在勒布朗(LeBlanc)法则破窗理论。

——节选《代码整洁之道》

20 世纪 80 年代末,有家公司写了个很流行的杀手应用,许多专业人士都买来用。然后,发布周期开始拉长。缺陷总是不能修复。装载时间越来越久,崩溃的几率也越来越大。至今我还记得自己在某天沮丧地关掉那个程序,从此再不用它。在那之后不久,该公司就关门大吉了。

20年后,我见到那家公司的一位早期雇员,问他当年发生了什么事。他的回答叫我愈发恐惧起来。原来,当时他们赶着推出产品,代码写得乱七八糟。特性越加越多,代码也越来越烂,最后再也没法管理这些代码了。是糟糕的代码毁了这家公司。

……我们有专用的词来形容这种事(被糟糕的代码困扰)沼泽……

你当然曾为糟糕的代码所困扰过。那么为什么要写糟糕的代码呢?是想快点完成吗?是要赶时间吗?有可能。或许你觉得自己要干好所需的时间不够;假使花时间清理代码,老板就会大发雷霆。或许你只是不耐烦再搞这套程序,期望早点结束。或许你看了看自己承诺要做的其他事,意识到得赶紧弄完手上的东西,好接着做下一件工作。这种事我们都干过。

我们都曾经瞟一眼自己亲手造成的混乱,决定弃之而不顾,走向新一天。我们都曾经看到自己的烂程序居然能运行,然后断言能运行的烂程序总比什么都没有强。我们都曾经说过有朝一日再回头清理。当然,在那些日子里,我们都没听过勒布朗(LeBlanc)法则:稍后等于永不(Later equals never)。

……

务实的 Dave Thomas 和Andy Hunt 从另一角度阐述了这种情况。他们提到破窗理论。窗户破损了的建筑让人觉得似乎无人照管。于是别人也再不关心。他们放任窗户继续破损。最终自己也参加破坏活动,在外墙上涂鸦,任垃圾堆积。一扇破损的窗户开辟了大厦走向倾颓的道路。

态度

一言以蔽之:代码变得糟糕,纯属我们的过错。我们应该用我们的专业性捍卫代码的整洁。

最下面的例子比较有意思。

——节选《代码整洁之道》

你是否遇到过某种严重到要花数个星期来做本来只需数小时即可完成的事的混乱状况?你是否见过本来只需做一行修改,结果却涉及上百个模块的情况?这种事太常见了。

怎么会发生这种事?理由多得很。我们抱怨需求变化背离了初期设计。我们哀叹进度太紧张,没法干好活。我们把问题归咎于那些愚蠢的经理、苛求的用户、没用的营销方式和那些电话消毒剂。不过,亲爱的呆伯特,我们是自作自受,我们太不专业了。

这话可不太中听。怎么会是自作自受呢?难道不关需求的事?难道不关进度的事?难道不关那些蠢经理和没用的营销手段的事?难道他们就不该负点责吗?

……多数经理想要知道实情,即便他们看起来不喜欢实情。…… 。他们(需求/产品经理)会奋力维护进度和需求;那是他们该干的。你则当以同等的热情维护代码。

再说明白些,假使你是位医生,病人请求你在给他做手术前别洗手,因为那会花太多时间,你会照办吗?本该是病人说了算;但医生却绝对应该拒绝遵从。为什么?因为医生比病人更了解疾病和感染的风险。医生如果按病人说的办,就是一种不专业的态度(甚至是犯罪)。

同理,程序员遵从不了解混乱风险的经理的意愿,也是不专业的做法。

注:原注:1847年1gnaz Semmclweis(伊纳兹・塞麦尔维斯)提出医生应洗手的建议时,遭到了反对,人们认为医生太忙,接诊时无暇洗手。

何为整洁

如果我们泛泛而谈何为整洁,那就太过空洞了;而如果就事论事,则不是我们这场分享可以容乃下的体量。在这里,我想分享三个伟大的观点

优雅且高效

20220219161311184.png

——Bjarne Stroustrup,C++语言发明者,C++ Programming Language(中译版《C++程序设计语言》)一书作者。

我喜欢优雅和高效的代码。代码逻辑应当直截了当,,叫缺陷难以隐藏;尽量减少依赖关系,使之便于维护;依据某种分层战略完善错误处理代码;性能调至最优,省得引诱别人做没规矩的优化,搞出一堆混乱来。整洁的代码只做好一件事。

——作者评价

Bjarne 用了“优雅”词。说得好!我MacBook上的词典提供了如下定义:令人愉悦的精致和简单。注意对“愉悦”一词的强调。

Bjarne显然认为整洁的代码读起来令人愉悦。读这种代码,就像见到手工精美的音乐盒或者设计精良的汽车一般,让你会心一笑。

整洁基于测试(第四次提到测试)

——Dave Thomas,OTI公司创始人,Eclipse 战略教父

整洁的代码应可由作者之外的开发者阅读和增补。它应有单元测试和验收测试。 它使用有意义的命名。它只提供一种而非多种做一件事的途径。它只有尽量少的依赖关系,且要明确地定义和提供清晰、尽量少的 API。代码应通过其字面表达含义,因为不同的语言导致并非所有必需信息均可以通过代码自身清晰表达。

——作者评论

Dave将整洁系于测试之上!要在十年之前,这会让人大跌眼镜。但测试驱动开发(Test Driven Development)已在行业中造成了深远影响,成为基础规程之一。Dave 说得对。没有测试的代码不干净。不管它有多优雅,不管有多可读、多易理解,微乎测试,其不洁亦可知也。

童子军

光把代码写好可不够。必须时时保持代码整洁。我们都见过代码随时间流逝而腐坏。我们应当更积极地阻止腐坏的发生。

借用美国童子军一条简单的军规,应用到我们的专业领域:

让营地比你来时更干净。

如果每次签入时,代码都比签出时干净,那么代码就不会腐坏。清理并不一定要花多少功夫,也许只是改好一个变量名,拆分一个有点过长的函数,消除一点点重复代码,清理一个嵌套 if 语句。

你想要为一个代码随时间流逝而越变越好的项目工作吗?你还能相信有其他更专业的做法吗?难道持续改进不是专业性的内在组成部分吗?

单元测试

单元测试代码也必须保持简洁。

我们在修改现有代码的时候,不免会让测试失败,这个时候我们就需要同步地去修改测试代码。测试代码是随着生产代码一起演进的。

测试代码越脏,改动测试代码越难,我们开发和写新测试的时间就越少。最后,维护测试代码可能会比维护生产代码更加困难。

测试所能带来的一切好处,都基于一份可以维护的测试代码;如果测试代码不可维护,测试代码就会慢慢丧失其权威性,我们最终就会抛弃测试,整个项目来到不可维护的状态。

总结

我想我们不应该轻信任何权威,对于任何重要的观点,我们应该进行交叉论证,求证多方。而恰巧的是,所有的这些书,都在告诉我们,在软件开发中,慢即是快,欲速则不达。更巧的是,每本书都在自己的领域中,提到了单元测试/* 自动化测试*。

单元测试

单元测试不能用来保证系统没有问题,但是事实上,没有任何测试能保证系统没有问题(软件工程的基本理论:测试能证明错误的存在,而不能证明错误不存在),只不过单元测试是写起来最快运行起来最快调试起来最快的效能最高的方式。

但同时,仅仅有单元测试是不够的。单元测试可以保证程序员编写的概念是正确的,却无法保证程序员的概念是正确的。

——节选《人月神话》

在编写任何代码之前,规格说明必须提交给外部测试小组,以详细地检查说明的完整性和明确性。如同Vyssotsky所说的,开发人员自己无法完成这项工作:“ 他们不会告诉你他们不懂。相反,他们乐于自己摸索出解决问题和澄清疑惑的办法。

而这些书中关于单元测试的内容都惊人地一致:单元测试是一个优秀的、具有生命力的程序必不可少的一部分。

  1. 测试可以通过缩短集成、Debug的时间,来极大地提高开发速度,且具有交互性的测试能更加高效地提高开发速度(与非交互性的测试相比,出自《人月神话》其他章节)。
  2. 软件的概念设计的最重要,但是有时候我们不能一次性设计好。但是如果有了自动化测试,我们可以按实现、测试、重构、优化的顺序,一步一步优化我们的程序。
  3. 所以,来一起写单测吧~!单测可以极快发掘琐碎细节中的错误、加速开发和Debug的速度、让我们放心地将不好的设计重构成优雅的设计,而不用担心潜在的错误,并以此一步步降低程序的复杂度,让我们做出更好的设计,写出更好的代码,付出更少的时间,加更少的班,睡更多的觉!

内在联系

《人月神话》

  1. 因为软件的固有特点:琐碎的细节,所以我们为了保证当下代码的正确性,也为了保证未来的修改不破坏现有的系统,我们要编写单元测试。
  2. 一个需求的开发中,理应有一半的时间用于测试。
  3. 组件经过单独的测试(单元测试),再执行集成测试,效率最高。
  4. 由于设计的复杂,或许我们不能一次就开发好新功能/需求(所以我们需要重构
  5. 由于软件会缓慢地腐败,所以我们不得不隔一段时间,对某些部分进行重新设计(所以我们需要重构
  6. 为了软件健康地发展,我们需要重构现有的功能,以面对变化(所以我们需要重构

《重构》

  1. 我们需要自动化测试才能重构。
  2. 重构的目的是让代码更加整洁(所以我们需要代码整洁之道
  3. 重构还有其他各种各样的好处,因此重构的提前:自动化测试,非常重要。

《代码整洁之道》

  1. 测试让软件富有生命力。(防止人月神话中的腐败)
  2. 测试也需要清晰整洁,易于维护。(对应人月神话中的复杂度)

警示

这些书警示了我们诸多事情,比如软件本身就会随着开发和修复而腐败;而不整洁的代码更是具有破窗效应,想象一下到处都充满了这种不整洁后,我们如何前进;测试是许多问题的良药,但是测试的所有好处都依赖和生产代码同样优秀的测试代码。

推荐阅读