你好,我是徐昊。今天我们来聊聊测试驱动开发(Test-Driven Development,TDD)。
测试驱动开发,顾名思义,就是将软件需求转化为一组自动化测试,然后再根据测试描绘的场景,逐步实现软件功能的开发方法。
在正式开始学习TDD之前,我想通过四节课的时间,来演示如何通过TDD的方式完成一段完整的功能,让你对TDD的做法有个感性的认识。毕竟,我想很多人对TDD心存质疑,最主要还是因为不光没吃过猪肉,其实也没见过猪跑吧。
为了让我的演示更有针对性,有些基本原则你需要先了解一下。TDD的创始人Kent Beck,在他的传世大作 Test-Driven Development by Example 的开篇中给出了TDD的基本原则:
不过在今时今日,我认为第二条应该改为“消除坏味道(Bad Smell)”。毕竟重复仅仅是一种坏味道,还有很多不是重复的坏味道。
那么根据TDD的基本原则,Kent Beck将开发工作分成了三步,也就是后世广为流传的测试驱动开发咒语——红/绿/重构(Red/Green/Refactoring):
然而红/绿/重构循环仅仅关注单个测试这个层面,它没有回答测试从何而来。于是很多尝试采用TDD的人都卡在了第零步:我该写哪些测试?于是在2006年前后我总结了任务分解法,将任务列表作为TDD的核心要素。
任务分解法的步骤如下:
那么TDD的整体工作流程如下图所示:
请花几分钟仔细记忆这个流程,它将会在后续所有示例题目中反复出现。
接下来,我会通过TDD来实现命令行参数解析的功能。这个练习源自Robert C. Martin的*Clean Code*第十四章的一个例子。需求描述如下:
我们中的大多数人都不得不时不时地解析一下命令行参数。如果我们没有一个方便的工具,那么我们就简单地处理一下传入main函数的字符串数组。有很多开源工具可以完成这个任务,但它们可能并不能完全满足我们的要求。所以我们再写一个吧。
传递给程序的参数由标志和值组成。标志应该是一个字符,前面有一个减号。每个标志都应该有零个或多个与之相关的值。例如:
-l -p 8080 -d /usr/logs
“l”(日志)没有相关的值,它是一个布尔标志,如果存在则为true,不存在则为false。“p”(端口)有一个整数值,“d”(目录)有一个字符串值。标志后面如果存在多个值,则该标志表示一个列表:
-g this is a list -d 1 2 -3 5
"g"表示一个字符串列表[“this”, “is”, “a”, “list”],“d"标志表示一个整数列表[1, 2, -3, 5]。
如果参数中没有指定某个标志,那么解析器应该指定一个默认值。例如,false代表布尔值,0代表数字,”"代表字符串,[]代表列表。如果给出的参数与模式不匹配,重要的是给出一个好的错误信息,准确地解释什么是错误的。
确保你的代码是可扩展的,即如何增加新的数值类型是直接和明显的。
那么接下来,就让我们结合任务分解法使用TDD来完成这个需求。
首先我们需要考虑,别人将以何种方式使用这段代码,也就是这段代码的整体对外接口部分。我们可以通过写测试的方式,来感受API的友好程度。
在确定了API的形式之后,我们需要大致构思如何实现这个功能:
在API与实现方式有了方向之后,我们就可以根据需求的描述对功能进行分解了。这里可以先不求全面,有个大致的范围即可:
那么先让我们选择最简单的任务,并通过红绿循环实现它。注意其中绿的环节中,我们是如何“不惜犯下任何罪恶”的!
之后的两个任务,也要以同样的方式进行:
到此为止,我们已经完成题目中要求的第一个功能点了:
至此为止,我们实现了第一个主要功能:支持由三个不同类型组合而成的命令行参数的解析。
可以看到,我们在进入TDD的红/绿/重构循环之前做了许多准备工作。我们先花费2~3分钟设计了API,2~3分钟构思了实现策略,然后在任务分解上花费了更多的时间,差不多有5分钟。到目前为止,编码的时间大约是15分钟。
这并不像很多原教旨主义TDD实践者所推崇的那样,完全依赖重构而不去做设计。然而以我二十年来实践TDD的经验来看,理解需求,并通过测试构成高效的节奏,是有效实施TDD的前提。特别是在有其他团队成员的情况下(结对或项目组),更需要如此。希望你能从今天起,更加注重“TDD的准备工作”。
下节课,我们将在这段代码的基础上开始重构,然后再逐步完成后续的开发。
如果在思考实现策略的时候,我们选择了其他的实现方式,那么任务分解会有什么不同?
如果你在学习过程中还有什么问题或想法,欢迎加入读者交流群。最后,也欢迎把你学习这节课的代码与体会分享在留言区,我们下节课再见!