TDD到底美不美?

发表于:2012-12-21来源:博客园作者:Todd Wei点击数: 标签:tdd
实际项目中,由于用户需求不确定性和外部环境不确定性,不要期望可以在实现之前完全明确需求,需求是在实际运行看到效果之后才逐步明确的;我们的开发过程必须能够敏捷地适应需求的变化,而TDD的Test First理念恰好与之矛盾。

  最近CoolShell上的一篇《TDD并不是看上去的那么美》引起了敏捷社区的高度关注和激励辩论。今天,InfoQ甚至专门举行了一个虚拟座谈会《TDD有多美?》,几位国内敏捷社区的名人专门就此问题展开了深入地讨论。不论结果如何,这种探讨和反思的精神还是非常值得赞赏的。事件实际上可以简单地归纳为一个有一定影响力的开发人员质疑TDD,一群敏捷社区名人对TDD进行解释和辩护。现在,就让我坚定地站在CoolShell一边,为对TDD的质疑和批判添砖加瓦吧!

  我们首先来看看TDD的核心理念是什么。第一是用例即规范(Specification by Example),即把测试用例作为需求规范的一种形式。传统的需求表达方式包括文档,Use Case等,而TDD强调通过测试用例来表达需求。另外,TDD的测试用例是黑盒的基于外部接口的,所以,它实际上又是对外部接口的设计。不把测试用例单纯地视为测试,而从需求和设计的角度来看测试用例是TDD与传统测试的一个重要区别。TDD的第二个重要理念是Test First,强调测试对于实现的驱动作用,先写测试用例,再实现和重构。Test First的实质是先理解清楚需求,并做好外部接口设计,把它转化为测试用例,然后再来实现和重构。

  如果说用例即规范还弥补了文档和Use Case在表达需求时的某些不足,具有一定的好处,那么Test First则有很大的问题,尤其在没有测试用例失败之前,不要写任何一行代码的极端方式则更是极端的错误。

  如果测试用例就是需求和设计,那么为什么不能先写出测试用例再来实现呢?这不是我们最熟悉的先需求再设计再编码吗?答案是:不能执行的测试用例(Test First)和能执行的测试用例有着天壤之别,你写出了测试用例不代表你就看到了运行的实际效果。

  不能执行的测试用例和写在纸上的文档相比对实现的指导意义不见得能好到哪里去!除非是一些很简单的情况下,在实际的软件开发中,你很难在没有执行测试用例的情况下写出真正符合最终需求的测试用例来。比如:你做一个页面,页面的效果需求和设计通常会在真正可以运行之后不断调整,在实现之前只能有一个大致的轮廓和方向,许多方面的细节要么是没想清楚,要么是完全没想到,不可能一蹴而就。如果片面强调测试对实现的驱动作用,那么实际上隐含了需求和设计的细节可以在实现之前明确下来的假设,这是非常不敏捷的和不现实的!

  Test First要求写测试用例时对软件需求有精确的了解,但实际软件开发过程中用户需求和外部环境的不确定性会导致软件需求难以把握和频繁变动。

  用户需求的不确定性是指需求无法在用户真正能运行看到效果之前明确下来。比如:让你开发一套Wow这样大型的游戏,你能想象游戏的效果是设计者一开始就想好了精确到每一个细节吗?对于游戏这样的软件,需求和设计不可能脱离实际运行纸上谈兵地产生。游戏的设计者通常只能借助文档、草图、Use Case等非精确的方式大致提出需求,先做出原型,在看到效果之后才能逐步地细化和明确,需求设计的增加和改变会伴随整个软件开发过程。

  另外,还有一种极端的情况是根本不存在精确的用户需求,比如:自动化翻译软件,你能在实现之前就把翻译效果用测试用例固定下来吗?存在绝对正确的翻译方法吗?最近,我们和国外一家大公司客户谈一个项目需求的时候,客户讲了这样一句话我们现阶段还无法提出很细致的需求,只有等你们拿出第一个版本,然后我们再逐步地调整细化。我们的客户没有宣称自己在做敏捷,但人家的思维方式多敏捷啊,不是什么一上来就明确需求,而且还要精确写出自动化测试用例。有人说这种情况我们仍然可以先根据自己的理解进行TDD,这样做可以:

  1. 基于测试用例和客户沟通明确需求;

  2. 驱动实现。我对此持不同看法,能执行的测试用例和不能执行的测试用例有着天壤之别,客户从测试用例根本无法获得真实运行的体验,你能想象苹果把iPhone的测试用例写在PPT上,给用户做一个演讲,用户就能给出关于iPhone设计的反馈了吗?要真正的用户反馈,就需要实打实的软件,这不正是敏捷的Working software over document的思想吗?另外,既然用户无法在实际体验之前提出反馈,那么开发人员在开发初期做的需求分析和设计都只是一个探索,随时可能调整甚至被推翻,不值得在实现之前进行自动化测试设计的投资。

  外部环境的不确定性是指"当我们的系统需要和外部系统集成时,关于外部系统行为的假设也无法在实际集成运行前完全确定"。例如,要做一套股票客户端连上交易所系统,因为交易所的行为会直接影响到客户端的开发,所以只有在弄清交易所行为的情况下才谈得上开发出高质量的客户端。如果采用测试驱动,编写了各种涉及交易所行为的测试用例,比如什么情况下发什么类型的消息,消息格式如何,如何交互等等,但是这些测试用例本身是否正确却需要打一个大大的问号!这一方面是由于很多交易所提供的协议都不够清晰或者有许多未明确定义的地方;另一方面即使协议没有问题,开发人员也可能由于单纯的失误或者缺乏相应领域的基本知识而把协议理解错。

  实际上,要真正弄清交易所的行为明确客户端的需求,最重要的手段还是在交易所提供的测试环境中跑集成测试。对于Test First来讲,测试用例本身的错误可以说是代价最大的,不仅浪费时间和精力,更重要的是还打击开发人员的士气,谁愿意来回折腾呢?但很不幸,实际情况是在最初没有明确交易所行为的时候Test First出来的测试用例随时可能在真实集成后被推翻,并且如果是比较高层的需求分析失误,那对整个架构设计来讲会是灾难性的后果。在实际开发中,我们的软件需要和其他系统集成的情况是非常普遍的,而期望在没有进行实际集成的情况下弄清外部系统的行为都是不现实和不敏捷的。

  所以,Test First需要对于被测系统的需求和环境有精确的了解,但由于需求不确定性和外部环境不确定性两大问题,Test First在很多时候都是不现实的。其实,Test First和瀑布式思想一脉相承,都强调需求先于实现,而忽略了软件需求的产生会受到实现的反馈,会在实际运行中不断调整探索完善。TDD无非是把需求分析的结果用测试用例表达,替代传统用文档表达需求,但从宏观上看,TDD和瀑布比是换汤不换药,这都不是真正的敏捷。

  除了简单情况,不存在脱离实现的需求,你能够在明确了需求之后就实现出一套Linux系统吗?既然你根本无法实现一套Linux系统,那么这样所谓的需求又有多大的意义呢?所以,能提出什么样的需求不能脱离你的实现能力。需求和实现之间不是简单的谁驱动谁,而是一种相互反馈的关系,这与需求用什么方式表达没有关系。正如瀑布模型无法在初始阶段做出完美的需求分析,TDD也无法在初始阶段做出完美的测试用例;不仅如此,自动化测试用例的开发维护成本还远高于文档。

原文转自:http://www.ltesting.net