测试驱动开发:入门、实战与进阶
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

前言

测试驱动开发是一种在编程时克服恐惧的方式。

——Kent Beck

你知道我们有多幸福吗?TDD已经伴随我们好多年了。

几十年之前,水星计划(Project Mercury)的开发者就已经开始在穿孔卡片上实践TDD了(参见:https://oreil.ly/pKpSZ)。世纪之交,xUnit库让更多的人开始采用TDD做开发。Test-Driven Development: By Example(《测试驱动开发》,Addison-Wesley Professional, 2002)一书的作者Kent Beck还开发过JUnit测试框架,他说自己并没有发明(invent)测试驱动开发,只不过是“重新发现”(rediscovered)了它而已(参见https://oreil.ly/zDyBr)。这当然是一种谦虚的说法,但也道出了事实——TDD的历史其实跟软件开发一样长。

既然如此,那为什么TDD没有成为编写代码的标准方式呢?在日程紧迫或者IT方面的预算必须削减的时候,在我们(这其实是笔者自己)想要“提升软件交付团队的速度”的时候,为什么TDD是首先被砍掉的方式呢?根据经验与实验数据(参见https://oreil.ly/2Xxyb),TDD能够降低缺陷数量、简化设计方案,而且能够让开发者对自己所编写的代码更有信心。既然有这么多立竿见影的好处,那为什么我们还会以各种理由弃用TDD呢?

许多人不愿意使用TDD,即便用了也会很快就放弃。为什么会这样呢?这些人给出的理由包括下面几条:

我不知道从哪里开始做TDD,也不知道怎样做TDD

最常见的原因恐怕就是对TDD缺乏认识,不愿意接触TDD。其实与其他技能类似,用测试驱动的方式编写代码也是需要学习的。许多开发者之所以不愿意学习TDD可能是因为缺乏外部诱因(例如缺少时间、资源、指导、勇气),也可能是因为缺乏内在动机(例如缺少克服不情愿心理或恐惧心理的动力)。

TDD只能用在那种为了教学而设计的小程序里面,或是为了招聘而举行的编程面试之中,它不适合编写现实工作中的代码。

这种说法不对,但是笔者可以理解为什么有人会这么说。大多数讲解测试驱动开发的教程(当然也包括本书)都会从大众熟知的领域里面选择相对简单的例子。想从一款部署在商用场合的应用程序(例如那种部署在金融机构、医疗管理系统或无人驾驶汽车里面的应用程序)中选择一段实际的代码,并根据这样的代码撰写文章或书籍是相当困难的。其中一个原因在于,现实工作中的代码大多数是私有的,并不开源。另一个原因则在于,文章或书籍的作者总是想选择一个受众最为广泛的领域来讲解。作者没有理由选用某个极为专业的领域来演示TDD,因为那样是不明智的。假如非要那样做,那么在开始之前,必须花很长的时间去解释那个领域的一些晦涩术语及行话。不过这样产生的效果与作者想要的效果相反,作者本来是想让大家理解、接受乃至喜爱TDD这种开发方式的。

虽然编写TDD教材时不便采用现实工作中的代码来举例,但是开发者在编写日常工作中的软件时,确实能够得益于测试驱动开发这一方法。最典型也最有力的一个例子可能就是JUnit框架自己的单元测试套件了(参见https://oreil.ly/UCPcg)。另外,Linux Kernel(Linux内核)应该算是世界上使用频度最高的软件了吧?它的代码也正在通过单元测试得以改进(参见https://oreil.ly/hBbq0)。

只要能在产品代码写出来之后补写测试代码就行了,要求开发者率先编写测试代码显得太过严苛或者迂腐。

这条理由要比“单元测试被高估了”(参见https://oreil.ly/Y7S5M)之类的说法听起来新鲜一些。写好产品代码之后再编写测试确实要比根本不写测试强。凡是能让开发者对他们所写的代码更有信心、减少意外的复杂性和促使他们写出确切文档的做法都值得提倡。然而,在编写产品代码之前率先编写单元测试则能够让开发者根本没有机会向代码中引入复杂性。

TDD以下面这两条务实的原则为基础指引我们做出更为简单的设计:

1.编写产品代码只是为了让无法通过的测试得以通过。

2.只在测试能够通过的前提下才去奋力地重构代码。

那么,这是否意味着TDD必定能让我们写出最为简单且符合需求的代码呢?其实并不一定。没有哪种做法、哪条规则、哪本书籍或哪个宣言能够做到这一点。能否运用TDD写出简单而有效的代码,其实得看开发者究竟是如何运用TDD的。

本书想要用三种编程语言来解释什么是测试驱动开发,并告诉大家怎样做测试驱动开发。笔者的目标是让开发者在日常工作中养成用TDD来编写代码的习惯,并坚持这一习惯。这个目标或许有点大,但我还是希望自己能够在某种程度上实现它。

什么是测试驱动开发

测试驱动开发是一种用来设计代码并调整其结构的技术,促使开发者在代码规模不断增加的过程中依然能够写出简洁的代码并对其所写的代码更有信心。

下面我们就看看这个定义所涉及的几个方面。

TDD是一种技术

TDD是一种技术。这种技术当然也蕴含着一套与代码有关的理念,这就是:

• 以简洁为本——简洁是尽量避开无谓之事的艺术[1]2

• 直白而清晰要比精巧更为重要。

• 编写整洁而不杂乱的代码是取得成功的关键因素。

虽说TDD源自这些理念,但它依然是一种实用的技术。它跟骑自行车、揉面或解微分方程一样,并不是每个人天生就会的,而是必须靠学习才能掌握。

本书不打算在其他地方重复讲述TDD背后的这套理念。笔者假定你已经同意该理念,或者愿意尝试TDD这种技术。

本书中大部分内容都依照这样三个环节来运用TDD:首先编写一个失败的单元测试;其次编写刚刚够用的生产代码,让该测试得以通过;最后花时间清理代码。大家将有相当多的机会来亲自尝试TDD这种技术。

总之,比较理想的结果是,你不仅学会了这种技术,而且还培养出让自己坚持使用该技术的信念。这就好比你不仅学会了骑自行车,而且还能经常提醒自己这是一种既锻炼身体又不污染环境的出行方式。

TDD是一种用来设计代码并调整其结构的技术

请注意,TDD并不是为了测试代码而测试代码。虽然我们会用单元测试驱使自己编写程序,但TDD的最终目标在于通过测试设计出更好的、结构更佳的代码。

这一点是很重要的。假如TDD单纯是为了测试而测试,那就无法有力地论述为何必须在编写业务代码之前先编写测试,如果是只要有测试代码就行,那么写完业务代码之后再写测试,不也一样吗?测试先行,是为了激励我们设计出更好的软件,因此,测试仅仅是促进该过程顺利执行的一种手段。TDD最后确实会形成一套单元测试,但这只不过是锦上添花,我们的主要成果在于有了一套简洁的设计方案。

那么,究竟怎样才能得到简洁的设计方案呢?这要通过红-绿-重构这三个环节来实现,具体细节会在第1章开头讲解。

TDD是崇尚简洁的

我们不能仅仅认为简洁是个虚幻的概念,因为在软件开发领域,它其实是可以度量出来的。比方说,我们可以从实现每个功能所需的平均代码行数、循环复杂度(参见https://oreil.ly/5Gj2b)、副作用、运行时库的大小以及内存占用量等指标里面选出一个或多个(也可另行采用其他指标)来度量简洁程度,这些指标越低,简洁程度就越高。

测试驱动开发逼着我们“用最简单的办法把事情做成”(也就是说,只写出刚刚能让所有测试得以通过的代码就好),这样的话,我们就总是能够在衡量简洁程度的那些指标上面取得高分。我们不会再因为“万一要用到”或“马上就得写”之类的理由去编写多余的代码。由于有了一个失败的测试摆在我们面前,因此我们只需写出能让该测试以及早前已经通过的那些测试得以通过的代码即可。测试先行是一种动力,促使我们把复杂的问题及早暴露出来。如果我们要开发的功能定义得不够明确,或者我们对该功能的理解有所偏差,那么很难写出良好的测试,这会促使我们在还没有开始编写任何一行生产代码之前,就先把这些与功能有关的问题给搞清楚。这正是TDD的真谛:如果能够通过测试来推进编程工作,那就可以把遇到的每一个复杂问题都清理掉。

当然,TDD的好处也并没有那么神奇,你不要想着一旦开始做测试驱动开发,你的编程时间、代码行数与缺陷数量就会立刻减半。它的好处在于让你打消多余的念头,别再根据自己虚构或臆想出来的需求去编写复杂的代码。由于你先写了一个无法通过的测试,因此你会用最直白的代码让该测试得以通过,也就是说,你会写出最简单,并且能够满足该测试所提需求的代码。

TDD能让开发者对代码更有信心

代码应该能让我们对项目更有信心才对,如果这些代码是我们自己写的,那么更应该是这样。这种信心或许不太好描述,但总之,它源自一种对预测能力的欣赏。如果某个东西或某件事情的行为是可以预测的,那我们就会对其比较有信心。比方说,如果街角的那家咖啡店,今天少收我几元钱,明天又多收我几元钱,那么就算我连续两天去那里消费,也还是有可能对店员的工作水平失去信心。如果能够看清某件事情的规律并预测其走向,那我们会觉得这比单纯拥有该物更有价值,人性就是如此。全世界运气最好的赌徒,哪怕在轮盘赌上面连赢十次,也不会说自己对这个轮盘“信任”或“有信心”吧?比起好运,我们总是更看重预测能力或者稳定发挥的能力。

测试驱动开发让我们对代码更有信心,因为每写一个新的测试,系统就会朝着新的方向迈进一步,让我们能够看到系统在这个新的方向之下表现得如何,这一点确实是我们在编写这个新的测试之前不知道的。这样的一套测试能够帮助我们避开回归故障(regression failure)[2]

由于有了一套越来越扎实的测试,因此我们能够更加确信代码的质量也会随着规模的扩大逐渐提升。

目标读者

本书是给开发者看的,或者说,是给编写软件的人看的。

这个职业有许多种叫法,例如“软件工程师”(software engineer)、“应用程序架构师”(application architect)、“运维工程师”(devops engineer)、“测试自动化工程师”(test automation engineer)、“程序员”(programmer)、“黑客”(hacker)、“说码语的人/码语者”(code whisperer)等。无论这些职衔是张扬还是谦逊,是活泼还是严肃,是传统还是前卫,拥有该职衔的人都具备这样一个特点:每天或者至少每周有一部分时间要坐在计算机前阅读或编写代码。

我用开发者(developer)这个词来称呼这群人,因为我本身就是个平凡的开发者,同时也觉得自己很荣幸有这个机会来做开发。

编写代码是我们可以想象的一种极为自由而平等的活动。从理论上来说,唯一的身体要求就是要有头脑。除此之外,年龄、性别、国籍与族裔等因素通通不是障碍。就算身体行动不便,也不妨碍编写代码。

然而,现实却不是这样简单而公平,因为并非每个人都能得到计算机资源。此外,单凭兴趣与努力,未必能够把编程坚持学下来,因为你可能会受到各种各样的打击,例如学习编程时所参照的那些软件写得很烂,所使用的那些硬件设计得很糟,或是由于其他许多因素而无法顺畅地学习。

笔者尽量让本书易于阅读,尤其是让身障人士能够方便地学习。因此,书中的图片都配有替代文字,以便让电子阅读器可以读出这些文字。书里的代码都可以通过GitHub获取,而且文风也很直白。

本书对读者的编程经验没有过高的要求,还没有彻底学会编程的人以及已经学会了编程的人,都可以阅读本书。如果你正在提升自己对笔者所选的某一种(或某几种)编程语言的熟悉程度,那当然更应该来读本书了。

然而,本书并不会讲解任何一门编程语言的基础知识,其中也包括本书所要使用的Go、JavaScript及Python语言。因此,读者必须能够读懂并且会使用至少一种编程语言。如果你完全是编程新手,那最好是先从这三门语言里面选一门,把该语言的编程基础巩固好,然后再读本书。

本书的受众涵盖各种水平的开发者——从刚学编程的人到很有经验的架构师。图P-1描绘了本书的读者范围(Kent Beck不在此列)。

图P-1:这本书适合各种水平的软件开发者阅读

编写代码可能是一件让人时喜时怒的事情。但即便在最为丧气的时候,你也总应该能找到一股乐观情绪与自信心理,觉得自己这次肯定会把代码写好。只要坚持下去,你就会发现阅读本书很有收获,而且会觉得用测试驱动的方式编写代码很有意思,希望你在读完本书之后能够继续采用这种方式编程。

阅读前准备

在设备与技术要求方面,你需要做到这样几条:

• 有一台能够连接互联网的计算机。

• 有权安装并删除该计算机之中的软件。也就是说,你对计算机的使用权不受限制,在大多数情况下,这意味着你需要成为该计算机的“Administrator”(管理员)或“Superuser”(超级用户)。

• 能够启动并使用shell环境(也就是命令行环境)、网页浏览器与文本编辑器,最好还能安装一套IDE(集成开发系统)。

• 已经(或者能够)安装本书所采用的三门语言之一所需要的运行时工具。

• 能够用本书所采用的三门语言之一来编写“Hello World”程序并运行该程序。

如何安装这些工具,详见0.1节。

如何阅读本书

本书的主题是“如何用Go、JavaScript与Python做测试驱动开发”,笔者所讲的概念全都适用于这三种语言,然而针对每一种具体的语言,还是会分开讲解。与学习其他技能类似,要想学习测试驱动开发,最好的办法就是在实践之中学习,也就是边练边学。笔者建议大家不仅要阅读书中的文字,而且要自己编写代码。我把这种学习方式叫作“跟着书走”(follow the book),它会让你每读一段就去学着编写代码,把刚读过的这段内容演练一遍。

要想最充分地利用本书,你应该分别用三种语言来编写这个名为Money的范例程序。

本书许多章都包含这样的一节,它适用于全部三种语言,该节包含三小节,分别描述每一种语言的代码应该如何编写。这三小节的标题与该小节所针对的语言是一致的,也就是说,讲Go语言的那一小节就叫作Go,讲JavaScript语言的那一小节就叫作JavaScript,讲Python语言的那一小节就叫作Python。每章最后会用一或两节来概括目前已经取得了哪些成绩,以及接下来应该做些什么。

但是,第5~7章比较特殊,因为这三章是每章专讲一种语言,而不是把三种语言全都放到该章的某一节里面讲解。

图P-2演示了本书的布局,以及用Go、JavaScript与Python语言来学习本书内容时所遵循的路线。

图P-2:阅读本书的流程

下面是阅读本书的几种推荐方式。

一次使用一种语言学习本书

如果你符合下面的一个或多个条件,那么笔者建议你采用种方式学习本书:

1.在使用其他两种语言之前,我特别想要先采用某种语言来编程。

2.我很好奇(或者很怀疑)如何使用这三种语言中的某一种语言做TDD。

3.我擅长一次只用一种语言学习,而不擅长同时采用多种语言学习。

你每次可以沿着图P-2中的一条线往下阅读。比方说,如果你很想先搞清楚怎样用Go语言做TDD,那么就把专门针对JavaScript与Python的那些章节跳过去。第二次阅读本书时换一种语言来学习,比如换用JavaScript语言。到了第三次,再换用一种语言,比如Python。当然你也可以改用另一套顺序把这本书读三遍。第二次与第三次读,应该比第一次快,但是要注意,每种语言可能都有一些特殊的地方。

如果以这种方式阅读本书,那么你会依次采用三种语言做TDD,这能够让你更真切地体会到TDD其实是一个原则,而不单单是对某一门语言的测试功能所做的运用。培养编写测试的习惯固然很重要,然而弄清楚测试驱动开发为什么在各种语言中都行得通是一件更为重要的事。

先同时使用两种语言学习一遍,然后再使用另一种语言学习

如果你符合下面所说的任意一个条件,那么笔者建议你采用这种方式学习本书的内容:

1.我想要用两种语言解决同一个问题,并在这两种解法(解决方案)之间对比。

2.我不太喜欢这三种语言里面的某一种,所以想先用另外两种语言把这本书学一遍,然后再用这种语言学习。

3.我可以同时采用两种语言学习,但要是三种就有些难了。

你可以同时沿着图P-2中的两条线把本书学习一遍。等到你用这两种语言将Money范例程序写好之后,再采用另外的语言来学习第二遍。

如果你已经决定先使用其中的两种语言,但还不知道应该选哪两种(或者说,还不知道应该把哪一种语言推迟到第二轮),那么可以根据下面这些建议来考虑:

1.你是不是想在动态类型的语言与静态类型的语言之间对比,而且不想使用技术栈太过繁杂的语言?如果是这样,那就先用Go与Python学习本书,然后再用JavaScript学习。

2.你是不是想用两种区别很大的方式来编写代码,而且愿意面对各种各样的技术栈?如果是这样,那就先用Go与JavaScript学习本书,然后再用Python学习。

3.你是不是想先在两种动态语言之间对比?如果是这样,那就先用JavaScript与Python学习,然后再用Go。

以这种方式学习本书,你很快就能看到用各种语言做TDD时的相同与不同之处。这些语言在语法与设计上的差别可能会让你采用明显不同的方式做TDD,但你同时可能也会感觉到TDD这种开发方式对代码的写法影响真的很大,无论具体使用哪种语言编写代码,做TDD跟不做TDD时的写法都完全不同。

同时使用三种语言学习本书

如果你属于下面三种情况之一,那么建议你采用这样的方式学习:

1.你觉得同时用这三种语言学习可以更好地了解它们之间的差异与相似之处。

2.你觉得把一本书从头到尾完整地读一遍要比沿着不同的路径读好几遍更为容易。

3.你在这三种语言上已经有了一些经验,但还没试过用其中任何一种语言做TDD。

如果你能同时用三种语言编写代码而不觉得眼花,那么建议你这样来学习本书。

无论用哪种方式读这本书,都必须注意写代码的时候可能会遇到一些与你的开发环境有关的具体困难。这本书的代码已经测试无误(而且它的持续集成也做得没有问题,参见https://github.com/saleem/tdd-book-code/actions),但这并不意味着这些代码在你的计算机上一次就能运行成功。(相反,几乎可以断定,你总是得费点功夫去解决一些很有意思的问题。)TDD的一个关键优势在于开发速度能够由你自己掌控。如果某一部分推进得比较艰难,那就把速度放慢。每次只前进一小步,这样更容易发现代码是在你推进到哪一步时出现错误的。编写软件的过程中,总是需要处理各种错乱的依赖关系,还要应对不够稳定的网络连接与蹩脚的工具,并忍受代码中的种种毛病。如果你觉得应付不过来,那就每次只处理一个具体的问题,只改动一点点代码。总之,你要记住,TDD是一种帮助你克服编程恐惧的方式。

本书所采用的格式

本书有两方面的格式需要解释,一个是印刷格式,另一个是措辞格式。

印刷格式

有的时候,在叙述之中也会出现代码中的一些词,例如class、interface或Exception等,这种词采用等宽字体印刷。采用这种字体是为了提醒你这些词在代码里面也是这样写的。

大段的代码会分成多个代码块,例如:

❶ 省略号表示与当前讲解的内容无关的代码,或被略去的一些输出信息。

代码块里的内容,要么是需要照原样输入的代码,要么就是程序所输出的一些信息。但是有两个地方例外。

1.代码里的省略号(...)表示这里略去了一些代码或输出文字,这些代码或文字与当前所要讲解的话题无关。不要把这个省略号也录入代码里,也不要认为它会出现在输出信息中。刚才那段代码就出现了省略号。

2.如果代码块表示的是输出信息,那么其中可能会出现一些临时的值(ephemeral value),例如内存地址、时间戳、持续时长、行号、自动生成的文件名等,你在自己的计算机上看到的这些值不太可能跟书里印的值一样。在阅读这种输出信息时,你可以略过这些具体的值,比方说,下面这个代码块里的内存地址,其具体数值就不重要:

表示技巧,这是一些对你编写代码有所帮助的建议。它们与正文分开书写,便于参考。

表示与主题有关的重要信息。这些信息通常是指向某些资源的超链接或附注,这样的资源会为该主题提供更多内容。

本书有许多章都会分别采用Go、JavaScript与Python这三种语言来深入讲解开发技术并讨论相关的代码。只有第5~7章例外,它们都只针对一门语言而写。为了把讨论每一种语言所用的那些文字隔开,本书会用一个标题来指明这种语言,并在页边添加一个与该语言相对应的图标,以提醒你接下来的内容是专门针对这种语言而写的。请记住下面这三种标题以及与之相配的图标:

• Go

• JavaScript

• Python

措辞格式

本书要讨论一些核心的软件开发概念,并用三种语言编写代码,以提升讨论效果。由于这三种语言各自的术语区别很大,因此想用同一套说法概括这样三个系列的术语是相当困难的。

比方说,Go语言没有类,也没有基于类的继承机制。而JavaScript语言的类型体系里则有基于原型的对象,这意味着它的所有东西其实都是对象,也包括我们一般认为应该是类的那种东西。Python支持基于类的对象,本书会用一种“比较传统的”手法来使用Python的对象[3]。探讨Go语言的时候,别说“我们要创建一个名叫Money的新类”,这种说法不仅糊涂,而且完全错误。

为了减少误会,笔者决定采用表P-1中这套比较通用的说法来指代各语言里的关键概念。

表P-1:本书所采用的一套通用术语

笔者之所以选用自己的这套术语,是因为不想特意使用其中某一种编程语言的那套说法来讲解本书所包含的概念。阅读本书最大的收获应该是让你意识到,任何一种编程语言都可以做测试驱动开发。

然而,本书在专门讲到三种语言里的某一种时,则会使用该语言本身的说法(这样的文字都出现在与该语言相应的标题之下)。比方说,在专门针对Go的那一部分里面,可能会出现“定义一个名为Money的新结构体”这样的话。上下文会使读者很清楚这条指令是针对特定语言的。

示例代码

可以从https://github.com/saleem/tdd-book-code下载示例代码。

这里的代码是为了帮助你更好地理解本书的内容。通常,可以在程序或文档中使用本书中的代码,而不需要联系O'Reilly获得许可,除非需要大段地复制代码。例如,使用本书中所提供的几个代码片段来编写一个程序不需要得到我们的许可,但销售或发布本书中的示例代码则需要获得许可。引用本书的示例代码来回答问题也不需要许可,将本书中的很大一部分示例代码放到自己的产品文档中则需要获得许可。

非常欢迎读者使用本书中的代码,希望(但不强制)注明出处。注明出处时包含书名、作者、出版社和ISBN,例如:

Learning Test-DrivenDevelopment,作者Saleem Siddiqui,由O'Reilly出版,书号978-1-098-10647-8。

如果读者觉得对示例代码的使用超出了上面所给出的许可范围,欢迎通过permissions@oreilly.com联系我们。

如何联系我们

要询问技术问题或对本书提出建议,请发送电子邮件至errata@oreilly.com.cn

本书配套网站https://oreil.ly/learningTDDbook上列出了勘误表、示例以及其他信息。

关于书籍、课程、会议和新闻的更多信息,请访问我们的网站http://oreilly.com

与TDD有关的几个“为什么”

对TDD(这里主要指对本书)的批评会以多种形式发表出来。其中有些形式确实很搞笑,比方说图P-3中的漫画。这几幅清新的幽默图画是Jim Kersey绘制的。

图P-3:TDD笑话一则——桥都没建好,让我怎么走(来源:https://robotkersey.com/

说认真的:大家对书的内容和结构有意见,其实很正常。下面就来回答几个这方面的问题。

为什么要使用Go、JavaScript与Python这三种语言

本书使用Go、JavaScript与Python这三种语言演示测试驱动开发。有人会问:为什么选这三种语言呢?

下面解释原因。

1.这三种语言能涵盖多种设计方案

如表P-2所示,这三种语言在各方面拥有不同的特性,能够涵盖很大一批设计方案。

表P-2:对比Go、JavaScript及Python语言

表P-2:对比Go、JavaScript及Python语言(续)

2.这三种语言比较流行

Python、JavaScript与Go是开发者最想学习的三种新语言,这可以从Stack Overflow网站在2017(https://oreil.ly/CbnCx)、2018(https://oreil.ly/uhhLx)、2019(https://oreil.ly/BdAQJ)与2020年(https://oreil.ly/mHqNs)的年度调查里看出来。图P-4是2020年的调查结果。

图P-4:开发者最想学习的新语言,数据来自Stack Overflow的调查报告

在2021年的Stack Overflow调查数据(参见https://oreil.ly/hzMVk)中,TypeScript爬到了第二位,把JavaScript与Go分别挤到了第三与第四位,Python依然处在榜首。

从语法上讲,TypeScript是JavaScript的超集(参见https://oreil.ly/aATAD)。因此可以说,每一个想要学习TypeScript语言的开发者其实都必须了解JavaScript。我希望使用TypeScript语言的开发者也认为本书的内容有价值。

3.个人原因

过去几年,笔者有机会参与许多项目,那些项目的技术栈都包含这三种语言之一。在跟其他开发者合作的过程中,我发现有许多人虽然想学习并实践TDD,但却不会寻找做TDD所需的资源(或者无法坚守做TDD的纪律)。他们打算实践TDD,然而不知道应该怎样做,或者找不到合适的时间来做。这种现象在有经验的开发者与“菜鸟”(noob)身上都会出现。

笔者希望,想用任何一种语言来学习并实践TDD的开发者都能把本书当成实践指南并从中获得灵感,而不希望把本书仅仅局限在Go、JavaScript与Python这三种语言的范围内。

为什么不采用其他语言来讲解

对初学者来说,有大量的编程语言可供考虑。所以,就算我们分别采用好几种语言来写一系列的教程,也只能涵盖这些编程语言之中的一小部分,因为开发者在日常工作中为了学术、商务及娱乐等目标而编写代码时,所采用的语言实在是太多了。

另外,Java语言已经有一本很棒的测试驱动开发教程了[4]。与其他许多开发者一样,笔者本人也从Kent Beck的那本杰作之中获得了许多启发,它让我迷上了TDD这门技艺。那本书是用一个关于钱的问题来举例的,这促使我在本书中也举了这样的例子。

我当然知道,针对其他许多种编程语言来写TDD教程也很有帮助。例如可以针对R语言、SQL语言,乃至COBOL语言来制作TDD指南。

这里提到COBOL语言并不是信口胡说。在2005年左右,笔者参与过一个项目,当时我演示了如何通过COBOLUnit在COBOL语言里做TDD。这是我用一种比我还要大十几岁的老式语言学到的最有趣的东西。

所以,笔者希望你能接过这个重任。你应该自己去学习如何用其他语言做测试驱动开发,把学到的经验教给大家,鼓励大家坚持使用TDD写程序,并遵守做TDD时的纪律和原则。你可以写博客,做开源项目,或者再写一本这方面的书。

为什么要有第0章

大多数编程语言都使用从0开始的计数方式来标识数组或其他可数序列之中的元素[5],本书所采用的这三种编程语言当然也是这样。从0开始计数已经是一种颇有历史的编程文化了,因此,章的编号从0开始算,可以说是在尊重这个悠久的传统。

另外,我还要向0这个数字本身致敬,有人可能觉得这个想法很疯狂。Charles Seife专门写过一本书来谈这个数。在追溯0的历史时,Seife指出,希腊人有一个用来表示“无”的数:

在那个【也就是希腊人的】思想世界里面,没有“无”这个东西。那里没有零。因此,西方在将近两千年的时间里都不接受零这个概念。这导致了很严重的后果。缺乏零的概念阻碍了数学发展,遏制了科学创新,而且连日历纪年也跟着遭殃。西方哲学家必须先打破他们固有的思想,然后才有可能接受零这个概念。

——Charles Seife,Zero: The Biography of a Dangerous Idea(《神奇的数字零》)

虽然这样说可能有点夸张,但测试驱动开发在编程界的地位确实与零这个概念在几千年前的西方哲学中的地位有些相似。总是有人不愿意接受它,这可能是因为他们一方面轻视这个概念,另一方面又觉得它很别扭,而且认为在什么产品代码都没有的情况下写测试实在是太麻烦了。“我为什么非得先写测试呢?我难道不清楚自己准备实现什么功能吗?”“测试驱动开发太迂腐了,这只是个理论而已,很难实践。”“把产品代码写完之后再测试,其实比先写测试更管用,反正至少不会比它差。”许多人用诸如此类的理由来反对TDD,这与当年那些人拒绝零这个概念何其相似,他们都认为这种东西是荒谬的。

其实书里出现第0章并非完全没有先例。Carol Schumacher写过一本书,名字就叫作Chapter Zero: Fundamental Notions of Abstract Mathematics(参见https://oreil.ly/nXJdV),许多大学课程都把它当作一本标准的高等数学教科书。现在来个无奖竞猜:起这样一个名字的书,会从第几章开始呢?

在Schumacher博士为这本书写的教师手册中有这样一句话,我觉得很有启发:

身为作者,你的任务是给读者提供适当的提示,让他们很容易就能循着这些提示理解你想要表达的意思。

——Carol Schumacher为Chapter Zero这本书所写的教师手册

笔者觉得这条建议很有道理。从实际效果看,第0章能够将该章与其后各章明确区分开。第1章会带着我们正式开始TDD之旅,我们要通过它与它后面的十几章来正式地学习TDD。然而在这之前,我们先要通过第0章了解启程前需要知道和准备的一些东西,以及这段旅程会带给我们什么收获。

把需要解释的问题说清之后,我们就可以进入第0章了。


[1] 简洁(simplicity)的定义,参见“Agile Manifesto”(敏捷软件开发宣言)十二项原则(https://agilemanifesto.org/principles.html)里面的第10项。(中文版的十二项原则,参见https://agilemanifesto.org/iso/zhcht/principles.htmlhttps://agilemanifesto.org/iso/zhchs/manifesto.html。)

[2]:也叫作回归失败,是指那种以前能够正常运作的功能在修改代码之后无法正常运作,或是某种较为通用的实现方式在修改之后变得不够通用的现象。所谓回归,意思是说以前的bug或以前那种比较差劲的代码现在又重新回来了。

[3] Python对OOP(面向对象编程)其实支持得相当流畅。比方说,你可以看看prototype.py(参见https://oreil.ly/ZKivt),这是个用Python实现的基于原型的对象系统。

[4]:应指Test Driven Development: By Example(《测试驱动开发》)。

[5] Lua显然是个例外。笔者的朋友Kent Spillner对此发表过一番妙论,我把它总结到了这里:https://oreil.ly/E9M41