我在参与的开发项目和咨询项目中都有实践TDD的经验,时至今日,在开发某些功能时,我仍然使用TDD来实现功能。虽然还没有达到将TDD溶解在开发血液中的自然习惯,但至少是我常用的编程工具之一,偶尔使用效果也不错。以下是我在大型团队中实施TDD时的一些想法。当时的整个咨询过程,至少在TDD的实现上,可以说是艰难的。今天,这些考虑仍然很重要。1、开发人员的质量意识包括管理者在内的开发人员的软件质量意识,往往建立在清晰可见的外部质量之上。评价一个开发人员的绩效,一个很重要的指标就是测试人员发现的缺陷数量。通常的软件开发思维总是认为开发人员不适合测试,因为他们总是站在自己的角度看问题,这可能会忽略真正需要测试的用例。这种想法给开发者一个错误的信号,认为自己不应该写测试,即使写了测试也写不好。众所周知,开发人员编写测试最重要的好处不是测试本身,而是可以促进开发、测试和需求分析师之间的交流和沟通。测试先行的方式还可以让开发者跳出实现的套路,站在业务的角度看问题,站在消费者的角度思考界面设计。如果开发人员总是懒洋洋地将测试职责委托给专门的测试人员,那么逐渐就会产生依赖心理。测试人员准确的测试当然可以保证质量,但是这种测试通常是黑盒测试,这里保证的质量主要是外部质量。而且,这种测试带来的反馈总是慢于开发进度,一旦发现缺陷,修复缺陷的成本就会变得更高。除了外部质量,软件质量与内部质量同样重要。软件成本等于开发成本和维护成本之和,维护成本的增加主要是由于内部质量差。当我们要求开发人员为原始代码编写单元测试时,我们总是觉得很难。主要原因是代码的可测试性不够好。要测试一个类,即使简单地创建它的对象也成为不可能完成的任务。在为这样的代码编写单元测试时,就好像陷入了蜘蛛网,被这些网缠住、纠缠,再怎么挣扎也摆脱不了;除非,我们可以迅速解决这个问题。但是,一旦采用这种粗暴的方法,系统就没有维护,而是重写了。测试先行的开发至少在某种程度上避免了这些问题。因为开发人员必须首先编写测试,这驱使开发人员强制性地考虑代码的可测试性。并且有了足够的测试保护,即使代码内部质量不好,也更容易重构。然而,这些好处在短期内是看不到的,如果团队无法达成共识,仅仅依靠一两个人去牢牢践行TDD,在测试覆盖率不足的情况下,无异于掉队桶。大多数开发人员在维护别人丑陋的代码时,可能会一遍一遍地骂,却不知道作为骂人的自己,其实是在重复被骂人的故事。2.需求分析与任务分解需求分析能力往往是开发人员的短板。开发人员已经养成了从技术实现的角度思考一切的习惯。实现一个网页,会想到怎么写JavaScript响应用户操作,怎么写CSS,却很少想到用户体验和操作流程。完成一个数据分析,总是想着数据的属性,转换和提取数据的算法,却没想到分析数据的价值和合理的流程。对于繁琐的需求描述,我们总是没有耐心去深入研究,而是在掌握了大致意思之后,才匆匆忙忙地开始开发实施。TDD要求我们在编写测试之前进行合理的任务分解。如果没有很好地理解需求,任务分解就无法顺利进行。这就提出了团队合作的问题。如果我们能从需求的源头上改进,或许TDD会变得更简单。比如故事的拆分比较合理,遵循UserStory的INVEST原则,那么要实现的故事在可测试性和独立性方面会得到更好的提升。如果需求分析师能把验收准则(AcceptanceCretiria)写得很清楚,任务分解也会变得容易。此外,如果需求分析人员可以参考甚至遵循SpecificationByExample的方法,使用Given-When-Then模型来描述每个用例场景;那么,任务分解不是很容易吗?或许TDD很难的最大原因是我们只关注开发人员,而忽略了需求分析师所起的关键作用。俗话说:“问渠有多清,好使有淡水之源”。我一直强调任务分解是有层次的。分析需求时,不能一下子就陷入繁琐的实现细节。从用户价值出发,先梳理出最外层的需求任务,然后抽丝剥茧,层层剖析,理清思路,把控复杂逻辑。任务分解基本上可以分为三个层次,即业务价值-->业务功能-->业务实现。这个层级是一个“递归”的状态,可以根据需求的复杂程度不断的往下拆分。任务分解是TDD的核心,是驱动设计和开发的重要力量,却一直被很多人所忽视。不能不说是一种误会和遗憾。3、测试先行的编程习惯,俗话说“江山易改,本性难改”。多年养成的开发习惯,不可能一蹴而就。这恰恰成为了很多人反对TDD的借口,为防御投下了坚硬的盾牌。但是,根据我个人的经验和观察,习惯的力量肯定是有的,但主要是对TDD方法的掌握程度和一些误区。如前所述,任务分解应该是TDD的出发点。大多数开发人员未能养成任务分解的习惯。所以,在改成先测试的时候,误以为测试一上来就应该写。因为思路不清晰,脑子乱七八糟,又不熟悉TDD,写测试变得困难,总感觉被绑住了,好像一只手被绑住了,好像在泥泞中挣扎.很多时候,他连三分之一的功力都发挥不出来。我们一直强调先测试。这很容易造成一种错觉,认为TDD必须从一开始就写测试,“简单设计”,所以没有设计。这使得习惯于提前设计的开发人员更加困难。那么,TDD需要提前设计吗?MartinFowler的文章IsDesignDead实际上清除了这个问题的根源。个人认为测试驱动开发还是可以提前设计的,看场景。设计不仅包括面向对象思想甚至设计模式的应用等技术设计,还包括对需求的分析和建模。如果不分析需求就开始写测试,好像不知道自己要去哪里,开始快速往前走,却发现反之亦然。测试驱动开发提倡的任务分解,其实就是需求分析。如何找到职责,确定职责的承担者,可以看作是建模设计。试驾就像是一种培养设计定力的手段,就像盘腿而坐的冥想者一样,试驾可以迫使你从测试的角度(即用户的角度)去思考界面,这样你就可以设计表达意图的界面。在开始测试驱动开发之前,做一个适度的超前设计,也有助于我们仔细思考技术实现方案。这并不反对测试驱动接口的设计。解决方案可能属于实施层面。如果过早考虑实现,会干扰我们对接口的判断;但完全忽略实现可能会导致设计方向的偏差。比如我们要实现XML消息到Java对象的转换。一种方案是通过jaxb将消息转换成Java对象,然后定义转换映射的Transformer,通过硬编码或反射的方式转换成相关的领域对象;然后在执行业务操作后,将返回的结果转换为另一个Jaxb对象。另一种解决方案是引入一个模板,例如StringTemplate或Velocity,定义转换的模板,然后替换它。这两种解决方案的不同直接影响了我们划分任务的方式。所以,在使用TDD的时候,千万不要一巴掌拍死,可以抱着开放的心态尝试一下。更重要的是,TDD并不是什么新花样。全世界都吃它,总要有适合它的场景吧。比如UI开发、交互协作控制逻辑、数据库开发、并发处理都不是使用TDD的好场景。4.重构能力TDD的核心是红-绿-重构。这意味着重构是TDD非常重要的一环,直接关系到TDD开发的代码质量。没有良好的重构能力,TDD就会消失。如果代码的内在品质是生命,那么重构就是灵魂,没有它,代码就没有灵性。大多数时候,在实施TDD时,您会因缺乏重构能力而陷入困境。重构的关键首先是如何识别代码的坏味道。这需要在代码阅读中多多练习,而不是死记硬背MartinFowler的书《重构》中总结的badtaste。当这些难闻的气味成为你的直觉,甚至像是一种与生俱来的能力,你就会降低对坏代码的容忍度。在你眼里,这些烂代码就是垃圾,必须清理干净,否则不能“安居乐业”。重构技术与代码气味一一对应。当有测试保证时,重构变得安全。但在可能的情况下,我们还是希望使用工具提供的自动重构功能,这样既能提高重构的效率,又能在一定程度上保证重构的安全性。当然,找到重构的节奏很重要,即小步向前,每次重构都要跑测试是个好习惯。如果能结合Git等分布式版本管理系统实现原子提交,会更方便。即使重构出现问题,也可以快速回到之前的版本快照。在TDD的过程中,配对自然是一个不错的选择。当一个人在敲键盘时,另一个人可以专注于代码的可读性,看看它是否有问题。毕竟两人的眼光应该更锐利一些,至少视野更开阔一些??。及时重构是很多重构实践中最重要的一点。不要让重组成为你未来偿还债务的杀手锏。你拖得越远,偿还债务的成本就越高。在重构方面,如果把重构拖到***,就需要更强的重构能力,因为程序结构会变得更复杂。当然,只要你的代码能够保证足够的覆盖率和更好的松散耦合,重构还是可行的。使用TDD基本可以满足这两个需求。但在成本方面,小步走才是重构的方式。5.Infrastructureforunittesting***说说单元测试的基础设施。很多时候,这可能不是问题;但很多时候,它会成为一个大问题。面对测试数据准备等问题,需要仔细分析,寻找解决方案。原则上可以找一些开源的测试框架,包括生成测试数据,模拟测试行为等,因为你遇到的问题,其他人可能以前也遇到过。这个世界上有很多聪明和愿意分享的程序员,不要把自己局限在公司的一个角落。睁开眼睛,看看世界。所谓“君子不生异,善假善事也”。优秀的程序员也是如此。也许,你会放弃TDD,因为你找到了更适合你的方法。【本文为专栏作家“张艺”原创稿件,转载请联系原作者】点此阅读更多该作者好文
