Harry J.W. Percival目前就职于PythonAnywhere,他在各种演讲、研讨会和开发者大会上积极推广测试驱动开发(TDD)。他在利物浦大学获得计算机科学硕士学位,在剑桥大学获得哲学硕士学位。Harry Percival著有《Python Web开发:测试驱动方法》一书,该书手把手教你从头开始开发一个真正的Web应用,并且展示使用Python完成测试驱动开发(TDD)的优势。你将学到如何在开发应用的每一个部分之前先编写和运行测试,然后再编写最少量的代码让测试通过——也就是应用TDD理念,写出简洁可用、赏心悦目的代码。

enter image description here

问:很多敏捷教练都表示训练新人做测试驱动开发(TDD)是一件辛苦而进度缓慢的事,并且收益也不是很大。你在《Python Web开发》中采用了哪些方法来避免这种情况?

这是一个很难的问题!教授TDD这样的方法是无法保证收效的,但是既然市面上有那么多关于TDD的书和教程,也就证明了人们一旦理解了这种方法,他们的编程方式就会发生改变,而新的方式会令他们感到更加欣喜,所以他们就有了分享的欲望。

让我的书区别于其他书的地方我认为主要有两点。首先是《Python Web开发》的“对话式”风格。与其花气力写一本学术性、讲座式的书,我在写作本书时,想象的是读者坐在我身边和我一起完成一个项目的情景。我就是这样学习TDD的:和专家结对编程。所以,我没有选择发表一系列课程或规则,而是通过真实的例子来演示如何使用TDD来解决问题,同时我还会告诉读者我脑子里想的是什么,我是如何做决定的,为什么我会使用某种测试方法。

第二点不同在于,我选用了非常实际的例子——真正的项目,用来构建具有真正功能的真正网站。虽然简单,但是很真实。不像很多教程那样,比如让你搭建一个“罗马数字翻译器”或其他抽象例子,虽然这样的示例乍一看让人觉得清楚明了,但是你很难把这样的东西应用到真实世界中。

问:从你的角度上说,学习TDD最有效的方法是什么?TDD的学习曲线是否陡峭?

可以这样说,TDD的理论很简单,但是实践很难。只要通过一个简单的例子就能轻松理解TDD的基本思想,你在任何线上教程中都能找到。但是把这种方法应用到真实世界的项目中可就难了。只有时间的磨砺才能让你在判断测试的好坏时找到感觉,什么是有用的,什么是没有成效的。遗憾的是,获得这种知识的唯一方法就是时间和经验,但是我会尽力帮助读者在正确的路上跑起来。

在这《Python Web开发》中,我努力让学习曲线尽可能地柔和——我每次只介绍一个概念,并且让前几章的内容短小而令人愉快,我坚持每章一个概念为的是让读者理解基本概念,哪怕我们开头学到的只是经过简化的不切实际的技巧。到了后来我们会开始学习“真正”的方法,这些技巧会稍微复杂一些。相比于提供一步到位的完美解决方案,我认为这种学习方法更好。在书的最后,读者将会接触到我和我的同事们每天都在使用的最为真材实料的TDD技巧。

问:你为什么选择Python作为讲解TDD的编程语言?Python是否具有某些其他语言无法比拟的独特优势?

我倾向于把Python作为教学语言是因为Python非常简单——它“不碍事”。Python代码非常易读,而且没有很多碍事的样板;没有你在Java中会看到的“public static void main %#¥%%¥”关键字。这些东西可能对于大型企业级应用是有效的(没有定论!),但是在教授编程方法的过程中却是不折不扣的绊脚石。的确,Python简单,利于教学,但是Python也很灵活,这也让很多TDD技巧变得简单了(比如模拟和修补,我在书中将会讲到)。

问:就Web开发而言,Python Web和ASP.NET Web有什么区别?它们各有哪些优缺点?

我必须承认,我对ASP.NET一无所知,所以我也没什么资格回答这个问题。我知道很多业务之所以使用了ASP.NET是因为它是某种常见的微软栈的一部分,但是我也知道很多硅谷创业公司都借助Python获得了巨大的成功(比如Youtube, Instagram, Reddit,等等)。

问:你认为Flask怎么样?你在Django和Flask之间如何抉择?

Flask很简单,但是Django有更多内置功能(或者我们有时会说“自带电池(功能齐备)”)。我把Flask用在了小型“微服务”应用上,但是对于大应用来说Django是一个很不错的选择,因为Django拥有很多强大的特性,比如认证、管理界面,以及各种第三方插件和应用。但是就像任何“一体适用”解决方案一样,有时效果很好,有时内置Django功能却并不是你真正想要的。所以要有所权衡,但是到最后,这个决策也没有那么重要。无论你用哪种工具都可以获得成功!

问:业务越复杂测试越多,有时甚至比开发时间还长,这样的情况对于小型创业公司来说肯定是不愿意看到的。关于如何平衡测试和开发,你有什么建议?

我想先退一步来论证这个问题。但是首先必须要说明,我认为复杂的项目并不一定意味着“成比例”的测试时间。但是为了讨论需要,我们先假定一段带有测试的代码需要两倍于没有测试的代码的时间(虽然有些人会告诉你,如果你很牛的话,写带有测试的代码会比没有测试的还快)。但是我们还是假定写测试会多花一倍的时间。如果为一个小型的简单项目写代码需要花费两倍的时间,那么为复杂的大型项目写代码也要花两倍的时间——比例不会变。

区别在于测试带来的好处。在一个简单的项目中,测试并不是特别有价值的。如果你的项目很简单,你可以轻松快速的完成手动测试。如果你想对代码进行大改动的话,比如升级核心模块或者重构中心算法,那么你不需要测试就能完成,只要花很少的时间你就能检查完毕,并且自信一切没有问题。

但是对大项目来说,就是另一码事了。想象一下你参与过的最大的项目,并且想象一下升级一个核心组件的情况——该框架绝对会出现在所有代码中,而你想为这个组件升级一个带有新API和新特性的新版本。没有测试,这将是一个几乎无法完成的任务。在最近的工作中,我们升级了我们的Django核心版本,多亏有测试,我们只花了几星期时间就完成了。到了最后,我们得以确保一切都能顺利运行,而且当我们把项目部署到客户端时,我们也有信心客户不会看到回归。但是如果没有测试的话,我们就无法完成。可以毫不夸张地说,如果没有测试我们连试都不会试,因为风险太大了。而且我们就得一直将就着用老版本Django,没准这个版本最终会老到不被支持的程度,并且还会产生各种各样的安全问题……

所以随着时间推移,到头来还是复杂的项目会收获TDD真正的好处。没有测试,你将不敢在你的系统上做太大的改动。你会回避重构,因为风险太大。技术债越积越多,做出新改动会越来越难,而项目上的工程师则会越来越闷闷不乐。

我在《Python Web开发》的前言中谈到了我在第一个项目中的经历。

在实践测试的过程中,这是最难的部分之一——这是一种投资,需要花费额外的时间和努力,虽然可以即刻提升代码,但是“真正”的好处却是在未来才能兑现的。所以只有团队和管理层具有前瞻性思维才能完成。虽然速度对于创业公司来说至关重要,但是如果公司因为欠下太多技术债而无法继续创新和响应市场需求的话,创业最终会走向失败。

问:在测试驱动开发中,每一个类,每一个方法都需要写测试用例吗?如何在不花费太多时间的情况下做到面面俱到?

我在书中的第4章谈到了这个问题。你需要找到自己的平衡点。你可以为自己辩护,如果一个功能规模很小而且无关紧要,可能就不需要测试了。但是危险在于:随着时间推移会发生什么?可能这个简单的功能会变得稍稍大一点,加了一个if语句。但是这个功能没有测试,而增加新测试就需要额外的努力,所以加入没有测试的新代码可能也没有关系。然后这个功能增加了一个for循环,一点一滴,变得越来越复杂,也越来越需要测试,但是由于每个阶段的心理屏障,这个功能可能永远都没有测试。所以也许最安全的做法就是在最开头的时候做测试,因为在已经有了测试的情况下增加新测试就会容易很多。毕竟,如果功能真的很简单的话,写第一个测试应该也难不到哪去吧?

问:我们知道测试驱动开发在小项目中是非常有效的,但是TDD是否适用于中型甚至大型项目?在大型项目中使用TDD时是否有哪些需要注意的事情?

正如我所说的,TDD的真正好处只有在项目达到一定规模和年限时才会显现出来。但是没错,确实有一些问题需要注意。第一个问题就是测试套件的速度。这件事很容易就会被做过头,而且有些人执着于测试速度,但是这个问题还是很有必要注意一下的。如果你有测试分散在低级“单元”测试和高级“功能”测试中,那么你应该担心的问题是:你的单元测试的运行时间何时会变成几分钟,或者功能测试何时会变成几小时。测试方法的核心就是在合理的情况下尽可能快地得到反馈。有时你会发现,你的测试时间不再随着应用的大小成比例增加,而是随着复杂度增加出现几何级增长。但是这个问题是可以解决的。我在《Python Web开发》后面的章节中谈到了如何取得平衡,特别是最后一章(第22章)。

问:遗留代码通常对于测试来说不太友好,如果要把TDD应用在遗留代码上,你有哪些建议?

我不能说自己是这方面的专家,但是关于这个问题Michael Feathers写了一本很不错的书,叫做《修改代码的艺术》。总体的建议是:不要绝望。在大量的已有代码库上增加测试可能看起来像个不可能的任务,但是这件事是可以完成的,只要随着时间循序渐进就好。一旦你决定开始增加测试,那么你就可以选择在每次向代码中加入新需求时增加测试,或者在每次出现需要修理的bug时增加测试。随着时间推移,测试覆盖量将会增加,而你得以开始重构应用,使其测试性更强,并且加速这个过程。

问:前端的测试驱动开发是否可以扩展到后端?软件工程中是否有哪些模型值得推荐?

在书中,我谈到了使用不同的测试层来测试前端(呈现在真实浏览器中的HTML渲染和Javascript性能)和后端(连接数据库并且处理数据的控制器和视图)。通过使用“高级”层的“真正”测试,我称其为功能测试,通过Selenium我们可以测试前端和整体应用。低级“单元”测试被用于测试前端Javascript和后端Python代码的单独组件。在《Python Web开发》的第18章和第19章中,我谈到了这方面的问题,这种模型名为“双循环TDD”和“外向内TDD”。

另外,我需要声明这本书并不只是关于TDD的。这本书讲的是一种用结构化、严格而且渐进的方式来做软件工程的完整方法。在我刚开始编程时,我只是在“码砖”——靠自己找答案,用“牛仔style”编程,为了让程序运行,我经常走捷径。最开始这样做没什么问题,但是随后你就会陷入麻烦。

在接下来的几年中我逐渐学会利用工具和技巧、以结构化的方法来编程,这就是我想在这本书中传达的。我说的是TDD,没错,但是也包括很多其他东西:使用Git这样的版本管理工具并且完成无数微小的渐进式的提交。虽然听起来像是毫不相关的主题,但事实上这是和测试驱动的工作方式紧密相关的,这种方式的核心就是微小而渐进的改变。我演示的一些“DevOps”思想讲的并不仅仅是构建和测试你自己的机器,同时也关于如何搭建登台环境,以及如何配合自动部署脚本使用测试来确保代码在生产环境下、服务器上、互联网上,以及本地计算机上顺利运行。我也讲到了如何集成第三方系统,以及使用持续集成服务器在一夜之间运行构建,我还谈到了一点关于在敏捷开发方法(如XP)中使用TDD的方法……我努力想要把我知道的所有“码砖”和“真正的软件开发”之间的区别,通过一本书传达出来。


更多精彩,加入图灵访谈微信!