很久没有维护blog,近两年新工作平平淡淡,生活迷迷糊糊,自己都不知道自己在做什么了,最近看了老赵 的用TDD方式实现老赵的SearchCriteriaBinder,一时手痒,也来凑凑热闹,赚赚人气.
我觉得老赵 之所谓觉得实施TDD很困惑,主要是没有把握好"Test Driven"和"Test First"之间的差别. 我们这里先不下结论.先演示我根据自己的理解,利用测试驱动开发SearchCriteriaBinder的过程.
1. 任务需求
2.设计Class和第一个Test Unit
相关的数据class:
第一个TestUnit
3.用例写好了,运行下用例,看它是否可以通过,不能通过就修改实现代码,让其通过测试.
很显然,当前的代码是无法通过测试,所以我们修改下SearchCriteriaBinder 的代码,让它可以通过这个测试.
4. 运行测试用例,通过了.
5. 根据需求获取新的用例.
如果你再也提炼不出用例,则证明任务已经完成.目前这个需求明显还没有完成,现在只针对一个固定条件进行测试/实现,而看需求,这个条件是需要被泛化(generalization) 利用三角法(Triangulation)我们获得了一个新的测试用例.
6.获得新用例之后,先运行测试,失败,必须重写实现让用例通过.
7. 重新运行两个测试用例,已经可以通过.
8. 重复5~7. 直到没有新的测试用例可以新增,任务完成.
这边的测试用例,还有一个测试类别和完整性的问题.我们后面再讨论.
9.重构,完善代码.
到了这一步,算是一个里程碑,接下来的步骤就会发生分歧.
哈哈,调侃一下.TDD不仅仅是驱动开发,同时也有重构,完善代码的步骤(Test-Code-Test-Refactoring-Test-Code)
重构就是在不改变可见行为的情况下,整理代码,去除坏味道,使其更加清晰可读.所以我们重构代码如下
10.重构之后运行所有测试用例,保证逻辑(可见的行为)没有被更改,这就是测试用例的另一个作用,保证原先的业务逻辑不被更改.
11.私用对象是否应该被测试.
原则上来说,私用对象是不需要测试.如果你想对他进行测试,则应该将它抽象出来,独立作为一个需求.这样子才不需要增加/修改对象本身的行为(public/protected)从而破坏本来的业务行为. 我们依然以SearchCriteriaBinder为例,进行下一步动作.
12.抽象出新的对象出来,并重构原来的对象.
13. 编写第一个Test Unit
14.因为重构了代码,所以必须运行所有的测试用例.你会发现除了新加的这个测试失败了,以前的用例都失败了.因为Tokenizer没有被实例化,所以我们修改下SearchCriteriaBinder的constructor
并实现Tokenizer的Tokenize方法
15. 重新运行所有的测试用例,通过
16. 接下来泛化Tokenizer的参数,重新添加测试用例,重复之前的步骤.(至于build要重构也是类似的步骤).
以上的步骤,就是我理解中的TDD的开发顺序.你可以下载演示代码自己进行尝试
http://cid-af3411fff50fdeaa.skydrive.live.com/embedicon.aspx/Public/Demo/SearchCriteriaBinder.rar
接下来,稍微八下理论方面的看法.
在老赵的文中,基本上是他已经先设计好了实现的思路/设计,然后再写出对应的测试用例,接下来进行编码.(好多"我想要单元测试",哈哈).以这样子的步骤进行TDD的开发,肯定是不会对原有的开发流程起到辅助作用的,相反,反而会增加很多负担,完全没办法发挥TDD的好处. 也就是老赵其实用的是"Test first",而不是靠Test来Dirven development的. 所以实践起来就觉得别扭.
难道,TDD都要这样子一步步来嘛?我设计能力比较强,一下子就可以把整体的轮廓都设计出来,总不能让我倒退回去,像菜鸟一样一步一步来吧?
没错,TDD是提倡小步前进,逐步测试,逐步实现. 但是其实这里面有步子迈得多大的问题. 步子迈得越大,就要求更好的综合设计能力/全局把握能力.
举个简单的例子. 如果刚开始TDD,或者对刚学开发的同学来说,他肯定是按照上面的步骤一步步来更保险的,但是对于一般熟练的人员来说,他就可以直接泛化参数,一下子写好两个测试用例.然后再通过两个用例直接驱动出实现来. 所以这个步子的大小完全取决于你对自己能力的定位^_^,有人只能一步跨过一条水沟,但是有人就是可以一步跳到河对岸,人比人气死人哦,千万别去比
另外一个问题,就是测试的完整性/覆盖率. 其实测试分类别的,一些是上层测试,一些是下层测试,如果你硬要把它们和在一起,会累死人的.比如上面的SearchCriteriaBinder的两个测试用例,从业务层面(上层),他们已经可以完整体现任务的需求,所以已经算是完整了. 如果你硬要把参数为空/参数结构非法也加进来,那应该让他们来驱动什么业务呢? 这些底层测试,是可以外包给类库,或者内部消化的. 你不能用他们来体现业务,驱动开发,当然也不是绝对的,可能某些特殊的情况正是业务需求也不一定.
还有另外一个典型的说法,就是阿不在帖子中回复说的
这样子的结果就是因为步子太大了,所以对全局观的把握能力要求太高了,导致自己觉得力不从心,好像能力不足的样子.也可是说,这样子的开发不是用测试用例驱动开发而得到的,而是需求经过脑袋(经验)进行整体设计之后,再往TDD上靠,自然就力不从心了,那么大的系统,脑袋的运算量毕竟有限啊.
总得来说,我认为要实践TDD,必须把握好二件事情:
1. 需求决定用例
2. 用例驱动开发.
特别是用例驱动开发,它就是我们通常说的"先有用例,再有实现",实现是用例驱动出来的,不是你脑袋先想出来的.脑袋想的应该是行为,比如SearchCriteriaBinder的BindModel方法.为了养成这种习惯,必须牢记一句话,从javaeye的gigix那边听来的"do the simplest thing to make the test pass, not the most stupid thing", 这也就是为什么我们为了通过第一个测试,直接hardcode的原因
Test Driven 和Test First是两个完全不一样的概念,目的不一样,效果也不一样.
本文基于署名 2.5 中国大陆许可协议发布,正品行货,如有雷同,皆为山寨,作者保留追究权利,但是在保留本文的署名 浪子(包含链接的情况下,允许进行转载,演绎或用于商业目的。如您有任何疑问或者授权方面的协商,请E-Mail/MSN联系我。