测试驱动开发 ―― 一种真正的工程化开发实践 (转载自孙鸣、邓辉)

自从软件危机的概念被提出以来,人们就在不断地探索解决之道。期间,这些探索者们从其他如硬件、建筑等相对成熟的行业借鉴了不少经验和知识,希望能够以工程化的方法解决软件领域所面对的难题,并提出了“软件工程”这样一个知识框架用以指导实践。但是,几十年过去了,结果表明,“软件工程”为我们带来的对软件开发本身反思方面的作用要远远大于解决软件危机方面的作用。

经历了这么长时间不那么成功的“软件工程”经历的人们不禁要问:为何在其他领域屡获成功的工程化经验,对于软件领域就不起作用了呢?RichardP.Gabriel在其“PatternsOfSoftware”一书中的一句话道出了最根本的原因:“……我们谈论了太多的关于软件的东西,对于软件这个东西本身,关注得太少了……”。确实是这样,我们从其他领域借鉴了太多的形式的东西,而对于软件以及软件开发活动本身的规律的认识上却思考不多。而工程化的根本就在于契合事物本身的规律。

关于软件本身的特点和规律,JackW.Reeves在其著名的论文“WhatIsSoftwareDesign?”中有深刻的论述。在此,仅列出几个重要的结论:
?*软件的设计成本非常低,因此很容易变得极其复杂
?*软件的构建成本基本为零,因此先构建出来然后验证是一种经济的做法
?*软件不受任何物理规律的约束,因此可能出现的故障情况比其他领域要高得多。
?*软件是非常精确的,一个bit的错误都会导致整个系统的崩溃。
这些规律对软件开发有着根本性的影响,主要表现在:需求范围的限定、设计成本的控制,需求满足性的验证以及软件产品本身的健康演进。

在我们实际的软件开发中,最令人头疼的问题莫过于两点:1、时间有限,要实现的功能太多;2、bug层出不穷,好像永远也改不完。一般来讲,造成这两个问题的根源都是由于违背了上述的软件本身的规律。下面我们来简单分析一下。

我们的需求分析结果是以软件需求规格说明书的形式交付的,描述的语言是自然语言。自然语言往往是含糊地。虽然对人来讲觉得可能已经很清楚了,但是对于真正去执行的计算机来说,精确度远远不够。因此,需求和实现之间存在着许多“不那么明确”的地方。由于软件的设计成本很低,因此,“负责任”开发人员在设计和实现时,会不经意地把这些不明确的地方最大化,造成需求和软件开发成本的隐式膨胀。如果我们要求每增加一个设计元素,那么设计开发人员就要去爬十层楼,相信我们的软件会减掉不少“赘肉”:)

此外,大家想想,我们的软件中的bug都是在什么阶段大量涌现的?没错,是联调阶段。正是由于软件的构建成本很低,因此,我们才得以在前期不那么有纪律地进行编码,然后把软件构建出来进行调试,验证。把软件先构建出来再验证本身没什么问题,问题在于由于软件不受任何物理规律约束,并且非常精确,因此任何一丁点的修改都可能造成难以预料的严重后果。而我们也缺乏一种有效的检查修改所造成影响的手段。测试部门的人工测试或者基于界面的robot测试非常的低效且容易遗漏。

那么我们怎么做才能符合软件本身的规律,从而在最大程度上避免上述问题呢?我们需要用和实现语言同样精确的语言来描述需求;我们需要用和实现语言同样精确的语言来描述验收条件;我们需要足够细粒度的检查点来固定我们的软件;这些检查点的运行要足够快、成本足够低以便于我们可以经常性的运行它们。这意味着什么呢?这意味着开发人员在编写实现代码前,要先用同样的语言编写测试,用测试代码来表达需求和验收条件,用测试来驱动整个的开发过程。这样做能有效地解决上述问题吗?我们来分析一下。

软件是精确地,容不得半点含糊。因此用编程语言描述需求可以避免那些似是而非的情况,如果觉得自己很清楚一个需求,那么请用编程语言写出一个测试用例,精确地表达需求场景和验收条件,如果写不出来,那就表明还没有搞清楚。此外,有了这个可以用编程语言描述的验收条件,就有了准确的判断完成的标准。一切以完成验收条件为目标,这样也就避免了因为没有精确目标而随手编写多余的代码。

另一方面,如果用测试来驱动我们的开发,那么每一个测试用例就充当了用以固定软件行为的检查点的角色。随着功能的不断增加,这些测试用例不断地累积,形成了一张坚固的安全网络。每当我们增加新功能时;每当我们更改bug时;每当我们重构代码时;我们都可以即时地运行这些测试,然后就可以立即得到反馈。有了这些检查点,一旦发现问题,可以很快地进行定位:问题一定出在上一次正常运行和这一次故障运行之间的修改上面,我们的测试用例的粒度越小,运行得越频繁,问题定位起来就越容易。

如果用测试来驱动开发,那么我们写出的代码将天生就具有可测试性。而可测试性是好的设计的重要标志。我们的实现代码将完全可以脱离预先设定的环境进行测试,测试的粒度完全可以根据需要进行调整。和完整的系统测试相比,这样的测试成本更低、运行更快、更灵活,更加可控,它们也是持续集成的重要基石。

软件开发就像是攀岩,既充满乐趣,又深具挑战,有时甚至危险重重。正如攀岩时最安全有效的方法是保证我们的四肢中每次仅移动有一个一样,我们在软件开发中也要遵守这样的纪律。在软件开发活动中,我们的四肢是什么呢?它们分别是:编写测试代码;编写实现代码;修改测试代码;修改实现代码。我们要非常清楚地知道我们正在移动得是四肢中的哪一个,并严格按照纪律执行。

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


什么是设计模式一套被反复使用、多数人知晓的、经过分类编目的、代码 设计经验 的总结;使用设计模式是为了 可重用 代码、让代码 更容易 被他人理解、保证代码 可靠性;设计模式使代码编制  真正工程化;设计模式使软件工程的 基石脉络, 如同大厦的结构一样;并不直接用来完成代码的编写,而是 描述 在各种不同情况下,要怎么解决问题的一种方案;能使不稳定依赖于相对稳定、具体依赖于相对抽象,避免引
单一职责原则定义(Single Responsibility Principle,SRP)一个对象应该只包含 单一的职责,并且该职责被完整地封装在一个类中。Every  Object should have  a single responsibility, and that responsibility should be entirely encapsulated by t
动态代理和CGLib代理分不清吗,看看这篇文章,写的非常好,强烈推荐。原文截图*************************************************************************************************************************原文文本************
适配器模式将一个类的接口转换成客户期望的另一个接口,使得原本接口不兼容的类可以相互合作。
策略模式定义了一系列算法族,并封装在类中,它们之间可以互相替换,此模式让算法的变化独立于使用算法的客户。
设计模式讲的是如何编写可扩展、可维护、可读的高质量代码,它是针对软件开发中经常遇到的一些设计问题,总结出来的一套通用的解决方案。
模板方法模式在一个方法中定义一个算法的骨架,而将一些步骤延迟到子类中,使得子类可以在不改变算法结构的情况下,重新定义算法中的某些步骤。
迭代器模式提供了一种方法,用于遍历集合对象中的元素,而又不暴露其内部的细节。
外观模式又叫门面模式,它提供了一个统一的(高层)接口,用来访问子系统中的一群接口,使得子系统更容易使用。
单例模式(Singleton Design Pattern)保证一个类只能有一个实例,并提供一个全局访问点。
组合模式可以将对象组合成树形结构来表示“整体-部分”的层次结构,使得客户可以用一致的方式处理个别对象和对象组合。
装饰者模式能够更灵活的,动态的给对象添加其它功能,而不需要修改任何现有的底层代码。
观察者模式(Observer Design Pattern)定义了对象之间的一对多依赖,当对象状态改变的时候,所有依赖者都会自动收到通知。
代理模式为对象提供一个代理,来控制对该对象的访问。代理模式在不改变原始类代码的情况下,通过引入代理类来给原始类附加功能。
工厂模式(Factory Design Pattern)可细分为三种,分别是简单工厂,工厂方法和抽象工厂,它们都是为了更好的创建对象。
状态模式允许对象在内部状态改变时,改变它的行为,对象看起来好像改变了它的类。
命令模式将请求封装为对象,能够支持请求的排队执行、记录日志、撤销等功能。
备忘录模式(Memento Pattern)保存一个对象的某个状态,以便在适当的时候恢复对象。备忘录模式属于行为型模式。 基本介绍 **意图:**在不破坏封装性的前提下,捕获一个对象的内部状态,并在该
顾名思义,责任链模式(Chain of Responsibility Pattern)为请求创建了一个接收者对象的链。这种模式给予请求的类型,对请求的发送者和接收者进行解耦。这种类型的设计模式属于行为
享元模式(Flyweight Pattern)(轻量级)(共享元素)主要用于减少创建对象的数量,以减少内存占用和提高性能。这种类型的设计模式属于结构型模式,它提供了减少对象数量从而改善应用所需的对象结