测试驱动开发实例
示例的需求描述 今天我们需要完成的需求是这样的: 对于一个给定的字符串,如果其中元音字母数目在整个字符串中的比例超过了30%,则将该元音字母替换成字符串mommy,额外的,在替换时,如果有连续的元音出现,则仅替换一次。 如果用实例化需求(Specification by Example)的方式来描述的话,需求可以转换成这样几条实例: hmm经过处理之后,应该保持原状 she经过处理之后,应该被替换为shmommy hear经过处理之后,应该被替换为hmommyr 当然,也可以加入一些边界值的检测,比如包含数字,大小写混杂的场景来验证,不过我们暂时可以将这些场景抛开,而仅仅关注与TDD本身。 为什么选择这个奇怪的例子 我记得在学校的时候,最害怕看到的就是书上举的各种离生活很远的例子,比如国外的书籍经常举汽车的例子,有引擎,有面板,但是作为一个只是能看到街上跑的车的穷学生,实际无法理解其中的关联关系。 其实,另外一种令人不那么舒服的例子是那种纯粹为了示例而编写的例子,现实世界中可能永远都不可能见到这样的代码,比如我们今天用到的例子。 当然,这种纯粹的例子也有其存在的价值:在脱离开复杂的细节之后,尽量的让读者专注于某个方面,从而达到对某方面练习的目的。因为跟现实完全相关的例子往往会变得复杂,很容易让读者转而去考虑复杂性本身,而忽略了对实践/练习的思考。 TDD步骤 通常的描述中,TDD有三个步骤: 先编写一个测试,由于此时没有任何实现,因此测试会失败 编写实现,以最快,最简单的方式,此时测试会通过 查看实现/测试,有没有改进的余地,如果有的话就用重构的方式来优化,并在重构之后保证测试通过 它的好处显而易见: 时时关注于实现功能,这样不会跑偏 每个功能都有测试覆盖,一旦改错,就会有测试失败 重构时更有信心,不用怕破坏掉已有的功能 测试即文档,而且是不会过期的文档,因为一旦实现变化,相关测试就会失败 使用TDD,一个重要的实践是测试先行。其实在编写任何测试之前,更重要的一个步骤是任务分解(Tasking)。只有当任务分解到恰当的粒度,整个过程才可能变得比较顺畅。 回到我们的例子,我们在知道整个需求的前提下,如何进行任务分解呢?作为实现优先的程序员,很可能会考虑诸如空字符串,元音比例是否到达30%等功能。这当然没有孰是孰非的问题,不过当需求本身就很复杂的情况下,这种直接面向实现的方式可能会导致越走越偏,考虑的越来越复杂,而耗费了几个小时的设计之后发现没有任何的实际进度。 如果是采用TDD的方式,下面的方式是一种可能的任务分解: 输入一个非元音字符,并预期返回字符本身 输入一个元音,并预期返回mommy 输入一个元音超过30%的字符串,并预期元音被替换 输入一个元音超过30%,并且存在连续元音的字符串,并预期只被替换一次 当然,这个任务分解可能并不是最好的,但是是一个比较清晰的分解。 实践 第一个任务 在本文中,我们将使用JavaScript来完成该功能的编写,测试框架采用了Jasmine,这里有一个模板项目,使用它你可以快速的启动,并跟着本教程一起实践。 根据任务分解,我们编写的第一个测试是: describe("mommify", function() { it("should return h when given h", function() { var expected = "h"; var result = mommify("h"); expect(result).toEqual(expected); }); }); 这个测试中有三行代码,这也是一般测试的标准写法,简称3A: 组织数据(Arrange) 执行需要被测的函数(Action) 验证结果(Assertion) 运行这个测试,此时由于还没有实现代码,因此Jasmine会报告失败。接下来我们用最快速的方法来编写实现,就目前来看,最简单的方式就是:...