在《测试驱动的JavaScript开发》一书第四章中提到「从测试中学习」,这是一个很值得推荐的学习方式。如果是为自己的应用代码设计断言编写测试,可以让我们在动手写代码之前清楚我们到底需要做什么,并且之后的重构可以有保证的进行;如果是为他人的应用代码设计断言编写测试,那将是很好的机会去学习他人的代码编写方式。
比如学习流行的 JavaScript 库的代码(例如 underscore.js )的时候可以采用这种方法。为 underscore.js
的函数编写测试,让测试通过,然后再自己着手编写函数,再进行测试;然后再比对 underscore.js
里函数的实现。此方法一方面可以练习编程能力,同时可以学习流行库里久经考验的写法。
继上文「Qunit快速上手」之后,本文将展示一次测试驱动开发实践:实现一个复制字符串本身的函数。采用的单元测试工具是 QUnit。
测试驱动开发遵循四个迭代步骤:
- 编写测试代码
- 运行测试,观察测试失败
- 使测试通过
- 重构
步骤一、测试驱动,先编写测试
JavaScript 字符串类(String)并未提供一个复制字符串本身的方法。我们要实现的是复制字符串本身,即函数应该可以将 "abcd"
复制成 n * "abcd"
,其中 n
代表要复制的次数。从代码的角度来看就是:
|
|
同时,当 n
的值小于等于 1
时,仅返还字符串本身。
|
|
这便是我们的断言。明确了函数的目标之后,我们就可以编写测试脚本(假定测试脚本名称为 string-test.js
,并且与源码 string.js
在同一目录下):
|
|
我们设计了 5 个断言。其中,前三个断言测试当 n
小于 1
的情况;最后两个测试是否正确的对字符串进行了 n
次复制。
步骤二、运行测试,观察测试失败
这一步将确认测试用例,确保测试用例的准确性。在执行测试的 html 中(假设是 tests.html
)引用测试脚本:
|
|
在浏览器中打开 tests.html
执行测试,可以看到测试失败了,因为我们还没有编写源代码。
步骤三、使测试通过
有了测试的保证,此时,我们可以采用最为直接的方式来实现功能(假定源代码文件为 string.js
):
|
|
新建一个字符串 duplicated
,循环 n
次,每次循环中添加待复制字符串(this
)到 duplicated
中。
源代码编写完成后,在 tests.html
中引用源代码文件并在浏览器中运行测试。
|
|
如果有遇到断言失败,说明源代码中逻辑存在问题,需要进行检查修改。
步骤四、重构
回看第三步的代码,在循环中每次让 duplicated
加上待复制字符串 this
。而 JavaScript 字符串的相加操作按照以下步骤进行:
- 创建存储 this 的字符串
- 创建存储 duplicated 当前值的字符串
- 创建存储 this + duplicated 结果的字符串
- 把 this 复制到结果字符串中
- 把 duplicated 当前的内容复制到结果字符串中
- 更新 duplicated,使他指向结果字符串
从步骤上可以看出字符串相加操作的效率偏低。所以考虑到数组的 join
方法,将代码重构如下:
|
|
重构的代码中,新建一个数组,循环n次将待复制的字符串添加到数组中,最后通过数组的 join
。再次运行测试,保证重构可以通过测试。
完成一次测试驱动迭代
到这里就是一个测试驱动开发的完整迭代:编写测试——运行测试,观察失败——使测试通过——重构。
后续
后续对于 duplicate
函数的修改都可以在 string-test.js
中用例的保证下进行安全的重构。比如我们再观察步骤四中重构后的代码:将待复制字符串作为数组的元素并采用数组的 join
方法将字符串连接以达到复制的目的。换种思路,如果我们能够将待复制字符串作为 join
方法的参数,那么只要新建一个长度为 n + 1
的数组就可以达到复制的目的。相比使用循环添加元素到数组中,后者从算法和代码量都可以达到精简!最终版本的 duplicated
方法如下:
|
|
运行测试,确保重构的代码能够通过测试。