最近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出来的测试用例随时可能在真实集成后被推翻,并且如果是比较高层的需求分析失误,那对整个架构设计来讲会是灾难性的后果。在实际开发中,我们的软件需要和其他系统集成的情况是非常普遍的,而期望在没有进行实际集成的情况下弄清外部系统的行为都是不现实和不敏捷的。

 

Test First需要对于被测系统的需求和环境有精确的了解,但由于需求不确定性和外部环境不确定性两大问题,Test First在很多时候都是不现实的。其实,Test First和瀑布式思想一脉相承,都强调需求先于实现,而忽略了软件需求的产生会受到实现的反馈,会在实际运行中不断调整探索完善。TDD无非是把需求分析的结果用测试用例表达,替代传统用文档表达需求,但从宏观上看,TDD和瀑布比是换汤不换药,这都不是真正的敏捷。除了简单情况,不存在脱离实现的需求,你能够在明确了需求之后就实现出一套Linux系统吗?既然你根本无法实现一套Linux系统,那么这样所谓的需求又有多大的意义呢?所以,能提出什么样的需求不能脱离你的实现能力。需求和实现之间不是简单的谁驱动谁,而是一种相互反馈的关系,这与需求用什么方式表达没有关系。正如瀑布模型无法在初始阶段做出完美的需求分析,TDD也无法在初始阶段做出完美的测试用例;不仅如此,自动化测试用例的开发维护成本还远高于文档。所以,在敏捷环境中,软件开发初期应该通过文档和用例等手段大致表达需求,实现之后在实际运行中体验效果,不断优化探索和明确需求和外部环境,当需求和对外部环境的认识达到一个比较稳定的程度才编写测试用例将需求固化下来。

 

Test First理念居然是和敏捷理念矛盾的!

所以,我认为Test First不符合敏捷开发的基本假设,而真正符合敏捷的理念是“需求和设计依赖于实现的反馈,需要在实际运行过程中根据效果不断探索调整得来的,不可能脱离实际运行写出真正符合最终需求的测试用例来”。所以,我们真正应该做的是尽快看到实际运行的效果,而自动化测试作为固化的需求和设计是在看到效果之后。在集成之前花太多精力进行测试驱动只会导致迟迟看不到实际运行效果(尤其是基于开发人员自己的假设编写大量单元测试用例),看到效果需要调整需求又会废掉或改掉一大堆的测试用例。实际上,越是外部的需求其变更带来的影响和代价越大,越是需要尽早明确。从宏观上看,TDD所谓的快速反馈实际上是加快内部反馈,延迟了外部反馈,这无异于本末倒置。而大量需要修改或作废的测试用例其实是一种很大的浪费,这和消除浪费的精益思想也是矛盾的!

 

 

 

在第一次集成运行测试之前不要写单元测试用例;自动化的验收测试用例则视编写和维护的代价而定,如果代价比较高,则应该采用文档和Use Case来描述需求,因为这两种方式比自动化的验收测试更容易维护。编写单元测试一定是在集成以后,这样才能首先得到外部反馈,尽量先保证做正确的事情,再正确地做事

 

下面这段话来自于InfoQ文章《Mock不是测试的银弹》:“在使用JMock框架后测试编写起来更容易,运行速度更快,也更稳定,然而出乎意料的是产品质量并没有如我们所预期的随着不断添加 的测试而变得愈加健壮,虽然产品代码的单元测试覆盖率超过了80%,然而在发布前进行全面测试时,常常发现严重的功能缺陷而不得不一轮轮的修复缺陷、回归 测试。为什么编写了大量的测试还会频繁出现这些问题呢? ”这描述的情况和我在实践中遇到的情况类似,不过很可惜文章并没有找到问题真正的原因。真正的原因不是什么Mock不Mock,而是TDD的单元测试是基于开发人员的假设,这些假设的测试即使全部通过代码覆盖率100%,到了集成测试发现假设根本不成立或者原先在单元层面很多情况没有考虑到,这又怎能保证高质量?在TDD的实践者中我见到过不少类似这样的,他们很认真,编写了很多单元测试用例,代码覆盖率也很高,但他们其实是有意无意在先正确地做事(单元测试),再做正确的事(集成测试),这就是本末倒置。

 

TDD在某些需求比较固定的场合是适用的,尤其是与具体业务关系不大的需求,比如:写一个通用的数据结构,实现一个通用算法。TDD的先关注需求和思考外部接口设计的理念也对促进开发人员的抽象思维有很大益处。另外,TDD通常也具有较高的代码覆盖率。本文的主要观点在于:实际项目中,由于用户需求不确定性和外部环境不确定性,不要期望可以在实现之前完全明确需求,需求是在实际运行看到效果之后才逐步明确的;我们的开发过程必须能够敏捷地适应需求的变化,而TDD的Test First理念恰好与之矛盾。所以,对于TDD不了解的朋友,我建议应该学习和实践TDD,从而获得其益处;同时我也提醒TDD存在理论上的缺陷,这是在实践中需要特别留意的。

 

相关文章: