TDD可以驱动设计吗?

前段时间有不少朋友发文讨论TDD引起了比较热烈的反响。我学习和实践TDD有近一年时间了,也希望把自己对TDD的理解拿出来讨论分享。本文讲讨论TDD的精髓和盲区,并希望引导TDD的初学者正确认识“TDD可以驱动得出更好的设计”这一著名论断。

TDD的精髓

提到TDD,最先浮现在我们脑海中的多半是这幅经典的迭代流程图:

tdd_cycle

不过,我认为这幅图的作用只在于与传统开发流程形成对比,更多的是一种形式上的东西。这幅图没有涵盖的东西是:到底怎么写测试用例?这个问题基本上已经取得了共识:TDD的测试用例一定是从功能需求来的。我们可以有多种不同的方式来表达功能需求,比如:自然语言的文档,UML的Use Case。而TDD的测试用例也应该是功能需求规范的一种表达方式。因此,在编写测试用例的过程中,我们把被测系统当成一个黑盒,把黑盒放在表达功能需求规范的某个应用场景中去使用。比如:对于Stack,功能需求是FILO,那么我们的测试用例就应该把Stack的对象当成黑盒放在检验FILO的场景中去使用。BDD正是看到了TDD关于如何写测试用例的描述不足,而明确提出测试用例一定是一个使用场景。我们不妨把TDD的测试用例视为一种可执行的需求规范,因此,它有利于对实现和重构形成快速反馈。如果要一句话描述TDD的精华,我会在TDD迭代流程图旁边加上一句Test Case is executable specification

 

TDD的盲区
TDD的测试用例在表达需求方面相比文档和Use Case有这么好的优势,那么是不是TDD就能保证开发出高质量的软件呢?我们先来思考一下,软件开发到底是什么驱动的?是测试在驱动吗?当然不是!测试还是由需求在驱动,即使在TDD中也不例外。

传统软件开发的动力模型:
分析用户需求 => 提炼出合乎需求的规范 => 根据规范进行设计 => 编写代码实现设计 => 编写测试用例 ...

而TDD软件开发的动力模型:
分析用户需求 => 提炼出合乎需求的规范 => 根据规范编写测试用例 => 编写代码通过测试 ...

明白软件开发是需求驱动其实还是很浅的理解。上面的动力模型描述过于粗线条,并没有真实反映程序开发的过程。系统A的需求决定了A的外部特征,在TDD中我们可以用相应的测试用例(包括验收测试和单元测试)来表达对A的需求。但为了克服系统复杂性,我们通常会将A划分为若干子系统a1,a2,a3,...,并通过一定的方式把各个子系统关联起来形成整体功能,这就是关于A的内部设计。系统A的内部设计产生出了子系统a1,a2,a3,...的需求。虽然在子系统的层次上,TDD同样可以帮助我们用测试用例表达需求,但TDD的盲区在于它没法驱动系统的内部设计!我们不可能通过TDD直接推出A的内部如何进行模块分解,子系统之间如何协作。我们常常听说“TDD驱动得出更好的设计”,这句话一定要正确理解。“TDD驱动得出更好的设计”包括两个方面:1.TDD首先关注系统的外部接口,在具体应用场景中检验接口并在实现之前对外部接口设计提供反馈;2. 系统内部设计已经完成以后,TDD能帮助对各子系统解耦,避免子系统间的非本质耦合。而关于系统A如何分解为子系统a1, a2, a3,TDD实在帮不上忙。

 

需求与设计的关系

如果把眼界放宽,任何系统都处于更大的系统之中,系统的需求往往是来自于上层系统的设计的。实际的开发过程也是上层的设计产生下层需求,在设计实现该需求的过程中又产生更下层的需求。如果说TDD测试用例的编写有科学的方法作依据,那么系统的设计则是一门艺术。明白需求与设计的关系才能把握TDD在软件开发中的定位,学习它的精髓,又不夸大它的效果。

 

总结

本文是自己学习和实践TDD一年时间的一个小结,文中错误不足欢迎批评指正,欢迎讨论!

posted on 2010-08-07 11:01  Todd Wei  阅读(2436)  评论(16编辑  收藏  举报