GHawk

#

敏捷软件开发 读书笔记 (4)——OO五大原则(3.LSP——里氏替换原则)

OCP作为OO的高层原则,主张使用“抽象(Abstraction)”和“多态(Polymorphism)”将设计中的静态结构改为动态结构,维持设计的封闭性。

“抽象”是语言提供的功能。“多态”由继承语义实现。

如此,问题产生了:“我们如何去度量继承关系的质量?”

Liskov于1987年提出了一个关于继承的原则“Inheritance should ensure that any property proved about supertype objects also holds for subtype objects.”——“继承必须确保超类所拥有的性质在子类中仍然成立。”也就是说,当一个子类的实例应该能够替换任何其超类的实例时,它们之间才具有is-A关系。

该原则称为Liskov Substitution Principle——里氏替换原则。林先生在上课时风趣地称之为“老鼠的儿子会打洞”。^_^

我们来研究一下LSP的实质。学习OO的时候,我们知道,一个对象是一组状态和一系列行为的组合体。状态是对象的内在特性,行为是对象的外在特性。LSP所表述的就是在同一个继承体系中的对象应该有共同的行为特征。

这一点上,表明了OO的继承与日常生活中的继承的本质区别。举一个例子:生物学的分类体系中把企鹅归属为鸟类。我们模仿这个体系,设计出这样的类和关系。

 lsp-fig1.jpg

类“鸟”中有个方法fly,企鹅自然也继承了这个方法,可是企鹅不能飞阿,于是,我们在企鹅的类中覆盖了fly方法,告诉方法的调用者:企鹅是不会飞的。这完全符合常理。但是,这违反了LSP,企鹅是鸟的子类,可是企鹅却不能飞!需要注意的是,此处的“鸟”已经不再是生物学中的鸟了,它是软件中的一个类、一个抽象。

有人会说,企鹅不能飞很正常啊,而且这样编写代码也能正常编译,只要在使用这个类的客户代码中加一句判断就行了。但是,这就是问题所在!首先,客户代码和“企鹅”的代码很有可能不是同时设计的,在当今软件外包一层又一层的开发模式下,你甚至根本不知道两个模块的原产地是哪里,也就谈不上去修改客户代码了。客户程序很可能是遗留系统的一部分,很可能已经不再维护,如果因为设计出这么一个“企鹅”而导致必须修改客户代码,谁应该承担这部分责任呢?(大概是上帝吧,谁叫他让“企鹅”不能飞的。^_^)“修改客户代码”直接违反了OCP,这就是OCP的重要性。违反LSP将使既有的设计不能封闭!

修正后的设计如下:

 lsp-fig2.jpg

但是,这就是LSP的全部了么?书中给了一个经典的例子,这又是一个不符合常理的例子:正方形不是一个长方形。这个悖论的详细内容能在网上找到,我就不多废话了。

LSP并没有提供解决这个问题的方案,而只是提出了这么一个问题。

于是,工程师们开始关注如何确保对象的行为。1988年,B. Meyer提出了Design by Contract(契约式设计)理论。DbC从形式化方法中借鉴了一套确保对象行为和自身状态的方法,其基本概念很简单:

  1. 每个方法调用之前,该方法应该校验传入参数的正确性,只有正确才能执行该方法,否则认为调用方违反契约,不予执行。这称为前置条件(Pre-condition)。
  2. 一旦通过前置条件的校验,方法必须执行,并且必须确保执行结果符合契约,这称之为后置条件(Post-condition)。
  3. 对象本身有一套对自身状态进行校验的检查条件,以确保该对象的本质不发生改变,这称之为不变式(Invariant)。

以上是单个对象的约束条件。为了满足LSP,当存在继承关系时,子类中方法的前置条件必须与超类中被覆盖的方法的前置条件相同或者更宽松;而子类中方法的后置条件必须与超类中被覆盖的方法的后置条件相同或者更为严格。

一些OO语言中的特性能够说明这一问题:

  • 继承并且覆盖超类方法的时候,子类中的方法的可见性必须等于或者大于超类中的方法的可见性,子类中的方法所抛出的受检异常只能是超类中对应方法所抛出的受检异常的子类。
    public class SuperClass{
        
    public void methodA() throws IOException{}
    }


    public class SubClassA extends SuperClass{
        
    //this overriding is illegal.
        private void methodA() throws Exception{}
    }


    public class SubClassB extends SuperClass{
        
    //this overriding is OK.
        public void methodA() throws FileNotFoundException{}
    }

  • 从Java5开始,子类中的方法的返回值也可以是对应的超类方法的返回值的子类。这叫做“协变”(Covariant)
    public class SuperClass {
        
    public Number caculate(){
            
    return null;
        }

    }


    public class SubClass extends SuperClass{
        
    //only compiles in Java 5 or later.
        public Integer caculate(){
            
    return null;
        }

    }

可以看出,以上这些特性都非常好地遵循了LSP。但是DbC呢?很遗憾,主流的面向对象语言(不论是动态语言还是静态语言)还没有加入对DbC的支持。但是随着AOP概念的产生,相信不久DbC也将成为OO语言的一个重要特性之一。

一些题外话:

前一阵子《敲响OO时代的丧钟》和《丧钟为谁而鸣》两篇文章引来了无数议论。其中提到了不少OO语言的不足。事实上,遵从LSP和OCP,不管是静态类型还是动态类型系统,只要是OO的设计,就应该对对象的行为有严格的约束。这个约束并不仅仅体现在方法签名上,而是这个具体行为的本身。这才是LSP和DbC的真谛。从这一点来说并不能说明“万事万物皆对象”的动态语言和“C++,Java”这种“按接口编程”语言的优劣,两类语言都有待于改进。庄兄对DJ的设想倒是开始引入DbC的概念了。这一点还是非常值得期待的。^_^
另外,接口的语义正被OCP、LSP、DbC这样的概念不断地强化,接口表达了对象行为之间的“契约”关系。而不是简单地作为一种实现多继承的语法糖。

posted @ 2006-01-18 18:12 GHawk 阅读(3970) | 评论 (2)编辑 收藏

敏捷软件开发 读书笔记 (3)——OO五大原则(2.OCP——开闭原则)

开闭原则很简单,一句话:“Closed for Modification; Open for Extension”——“对变更关闭;对扩展开放”。开闭原则其实没什么好讲的,我将其归结为一个高层次的设计总则。就这一点来讲,OCP的地位应该比SRP优先。

OCP的动机很简单:软件是变化的。不论是优质的设计还是低劣的设计都无法回避这一问题。OCP说明了软件设计应该尽可能地使架构稳定而又容易满足不同的需求。

为什么要OCP?答案也很简单——重用。

“重用”,并不是什么软件工程的专业词汇,它是工程界所共用的词汇。早在软件出现前,工程师们就在实践“重用”了。比如机械产品,通过零部件的组装得到最终的能够使用的工具。由于机械部件的设计和制造过程是极其复杂的,所以互换性是一个重要的特性。一辆车可以用不同的发动机、不同的变速箱、不同的轮胎……很多东西我们直接买来装上就可以了。这也是一个OCP的例子。(可能是由于我是搞机械出身的吧,所以就举些机械方面的例子^_^)。

如何在OO中引入OCP原则?把对实体的依赖改为对抽象的依赖就行了。下面的例子说明了这个过程:

05赛季的时候,一辆F1赛车有一台V10引擎。但是到了06赛季,国际汽联修改了规则,一辆F1赛车只能安装一台V8引擎。车队很快投入了新赛车的研发,不幸的是,从工程师那里得到消息,旧车身的设计不能够装进新研发的引擎。我们不得不为新的引擎重新打造车身,于是一辆新的赛车诞生了。但是,麻烦的事接踵而来,国际汽联频频修改规则,搞得设计师在“赛车”上改了又改,最终变得不成样子,只能把它废弃。

OCP-fig1.JPG

为了能够重用这辆昂贵的赛车,工程师们提出了解决方案:首先,在车身的设计上预留出安装引擎的位置和管线。然后,根据这些设计好的规范设计引擎(或是引擎的适配器)。于是,新的赛车设计方案就这样诞生了。

 OCP-fig2.JPG

显然,通过重构,这里应用的是一个典型的Bridge模式。这个实现的关键之处在于我们预先给引擎留出了位置!我们不必因为对引擎的规则的频频变更而制造相当多的车身,而是尽可能地沿用和改良现有的车身。
说到这里,想说一说OO设计的一个误区。
学习OO语言的时候,为了能够说明“继承”(或者说“is-a”)这个概念,教科书上经常用实际生活中的例子来解释。比如汽车是车,电车是车,F1赛车是汽车,所以车是汽车、电车、F1赛车的上层抽象。这个例子并没有错。问题是,这样的例子过于“形象”了!如果OO设计直接就可以将现实生活中的概念引用过来,那也就不需要什么软件工程师了!OO设计的关键概念是抽象。如果没有抽象,那所有的软件工程师的努力都是徒劳的。因为如果没有抽象,我们只能去构造世界中每一个对象。上面这个例子中,我们应该看到“引擎”这个抽象的存在,因为车队的工程师们为它预留了位置,为它制定了设计规范。
上面这个设计也实现了后面要说的DIP(依赖倒置原则)。但是请记住,OCP是OO设计原则中高层次的原则,其余的原则对OCP提供了不同程度的支持。为了实现OCP,我们会自觉或者不自觉地用到其它原则或是诸如Bridge、Decorator等设计模式。然而,对于一个应用系统而言,实现OCP并不是设计目的,我们所希望的只是一个稳定的架构。所以对OCP的追求也应该适可而止,不要陷入过渡设计。正如Martin本人所说:“No significant program can be 100% closed.”“Closure not complete but strategic”

(下一篇就要讲LSP了,我觉得这是意义最为重要的OO设计原则,它直指当今主流OO语言的软肋,点出了OO设计的精髓。)

posted @ 2006-01-18 00:26 GHawk 阅读(7574) | 评论 (7)编辑 收藏

开源 Java 测试框架(转)

from  http://blog.csdn.net/wangyihust/archive/2006/01/02/568616.aspx

 JUnit   

JUnit是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework)。Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。

http://www.junit.org/

 Cactus   

Cactus是一个基于JUnit框架的简单测试框架,用来单元测试服务端Java代码。Cactus框架的主要目标是能够单元测试服务端的使用Servlet对象的Java方法如HttpServletRequest,HttpServletResponse,HttpSession等。

http://jakarta.apache.org/cactus/

 Abbot   

Abbot是一个用来测试Java GUIs的框架。用简单的基于XML的脚本或者Java代码,你就可以开始一个GUI。

http://abbot.sourceforge.net/

 JUnitPerf   

Junitperf实际是junit的一个decorator,通过编写用于junitperf的单元测试,我们也可使测试过程自动化。

http://www.clarkware.com/software/JUnitPerf.html

 DbUnit   

DbUnit是为数据库驱动的项目提供的一个对JUnit 的扩展,除了提供一些常用功能,它可以将你的数据库置于一个测试轮回之间的状态。

http://dbunit.sourceforge.net/

 Mockrunner   

Mockrunner用在J2EE环境中进行应用程序的单元测试。它不仅支持Struts actions, servlets,过滤器和标签类还包括一个JDBC和一个JMS测试框架,可以用于测试基于EJB的应用程序。

http://mockrunner.sourceforge.net/index.html

 DBMonster   

DBMonster是一个用生成随机数据来测试SQL数据库的压力测试工具。

http://dbmonster.kernelpanic.pl/

 MockEJB   

MockEJB是一个不需要EJB容器就能运行EJB并进行测试的轻量级框架。

http://mockejb.sourceforge.net/

 StrutsTestCase   

StrutsTestCase 是Junit TestCase类的扩展,提供基于Struts框架的代码测试。StrutsTestCase同时提供Mock 对象方法和Cactus方法用来实际运行Struts ActionServlet,你可以通过运行servlet引擎来测试。因为StrutsTestCase使用ActionServlet控制器来测试你的代码,因此你不仅可以测试Action对象的实现,而且可以测试mappings,from beans以及forwards声明。StrutsTestCase不启动servlet容器来测试struts应用程序(容器外测试)也属于Mock对象测试,但是与EasyMock不同的是,EasyMock是提供了创建Mock对象的API,而StrutsTest则是专门负责测试Struts应用程序的Mock对象测试框架。

http://strutstestcase.sourceforge.net/

 JFCUnit   

JFCUnit使得你能够为Java偏移应用程序编写测试例子。它为从用代码打开的窗口上获得句柄提供了支持;为在一个部件层次定位部件提供支持;为在部件中发起事件(例如按一个按钮)以及以线程安全方式处理部件测试提供支持。

http://jfcunit.sourceforge.net/

 JTestCase   

JTestCase 使用XML文件来组织多测试案例数据,声明条件(操作和期望的结果),提供了一套易于使用的方法来检索XML中的测试案例,按照数据文件的定义来声明结果。

http://jtestcase.sourceforge.net/

 SQLUnit   

SQLUnit是一个单元测试框架,用于对数据库存储过程进行回归测试。用 Java/JUnit/XML开发。

http://sqlunit.sourceforge.net

 JTR   

JTR (Java Test Runner)是一个开源的基于反转控制(IOC)的J2EE测试框架。它允许你构建复杂的J2EE测试套件(Test Suites)并连到应用服务器执行测试,可以包括多个测试实例。JTR的licensed是GPL协议。

http://jtrunner.sourceforge.net/

 Marathon   

Marathon是一个针对使用Java/Swing开发GUI应用程序的测试框架,它由recorder, runner 和 editor组成,测试脚本是python代码。Marathon的焦点是放在最终用户的测试上。

http://marathonman.sourceforge.net

 TestNG   

TestNG是根据JUnit 和 NUnit思想而构建的一个测试框架,但是TestNG增加了许多新的功能使得它变得更加强大与容易使用比如:
*支持JSR 175注释(JDK 1.4利用JavaDoc注释同样也支持)
*灵活的Test配置
*支持默认的runtime和logging JDK功能
*强大的执行模型(不再TestSuite)
*支持独立的测试方法。

http://testng.org/

 Surrogate Test framework   

Surrogate Test framework是一个值得称赞单元测试框架,特别适合于大型,复杂Java系统的单元测试。这个框架能与JUnit,MockEJB和各种支持模拟对象(mock object )的测试工具无缝给合。这个框架基于AspectJ技术。

http://surrogate.sourceforge.net

 MockCreator   

MockCreator可以为给定的interface或class生成模拟对象(Mock object)的源码。

http://mockcreator.sourceforge.net/

 jMock   

jMock利用mock objects思想来对Java code进行测试。jMock具有以下特点:容易扩展,让你快速简单地定义mock objects,因此不必打破程序间的关联,让你定义灵活的超越对象之间交互作用而带来测试局限,减少你测试地脆弱性。

http://www.jmock.org/

 EasyMock   

EasyMock为Mock Objects提供接口并在JUnit测试中利用Java的proxy设计模式生成它们的实例。EasyMock最适合于测试驱动开发。

http://www.easymock.org/

 The Grinder   

The Grinder是一个负载测试框架。在BSD开源协议下免费使用。

http://grinder.sourceforge.net/

 XMLUnit   

XMLUnit不仅有Java版本的还有.Net版本的。Java开发的XMLUnit提供了两个JUnit 扩展类XMLAssert和XMLTestCase,和一组支持的类。这些类可以用来比较两张XML之间的不同之处,展示XML利用XSLT来,校验XML,求得XPath表达式在XML中的值,遍历XML中的某一节点利DOM展开。

http://xmlunit.sourceforge.net/

 Jameleon   

Jameleon一个自动化测试工具。它被用来测试各种各样的应用程序,所以它被设计成插件模式。为了使整个测试过程变得简单Jameleon提供了一个GUI,因此Jameleon实现了一个Swing 插件。

http://jameleon.sourceforge.net/index.html

 J2MEUnit   

J2MEUnit是应用在J2ME应用程序的一个单元测试框架。它基于JUnit。

http://j2meunit.sourceforge.net/

 Jetif   

Jetif是一个用纯Java实现的回归测试框架。它为Java程序单元测试以及功能测试提供了一个简单而且可 伸缩的架构,可以用于个人开发或企业级开发的测试。它容易使用,功能强大,而且拥有一些企业级测试的重要功能。Jetif来源于JUnit, JTestCase以及TestNG的启发,有几个基本的概念直接来自于JUnit, 比如说断言机制,Test Listener的概念,因此从JUnit转到Jetif是非常容易的。

http://jetif.sourceforge.net/

 GroboUtils   

GroboUtils使得扩展Java测试变得可能。它包括用在Java不同方面测试的多个子项目。在GroboUtils中最常被到的工具是:多线程测试(multi-threaded tests),整体单元测试(hierarchial unit tests),代码覆盖工具(code coverage tool)。

http://groboutils.sourceforge.net/

 Testare   

TESTARE是用来简化分布式应用程序(比如:在SERVLETS,JMS listeners, CORBA ORBs或RMI环境下)测试开发过程的一个测试框架。

https://testare.dev.java.net/

posted @ 2006-01-10 10:41 GHawk 阅读(1247) | 评论 (0)编辑 收藏

敏捷软件开发 读书笔记 (2)——OO五大原则(1.SRP 单一职责原则)

      一点说明:OO的五大原则是指SRP、OCP、LSP、DIP、ISP。这五个原则是书中所提到的。除此之外,书中还提到一些高层次的原则用于组织高层的设计元素,这些放到下次再写。当然,OO设计的原则可能不止这五个,希望大家多提宝贵意见,多多交流。

      在学习和使用OO设计的时候,我们应该明白:OO的出现使得软件工程师们能够用更接近真实世界的方法描述软件系统。然而,软件毕竟是建立在抽象层次上的东西,再怎么接近真实,也不能替代真实或被真实替代。

      OO设计的五大原则之间并不是相互孤立的。彼此间存在着一定关联,一个可以是另一个原则的加强或是基础。违反其中的某一个,可能同时违反了其余的原则。因此应该把这些原则融会贯通,牢记在心!

1. SRP(Single Responsibility Principle 单一职责原则)
      单一职责很容易理解,也很容易实现。所谓单一职责,就是一个设计元素只做一件事。什么是“只做一件事”?简单说就是少管闲事。现实中就是如此,如果要你专心做一件事情,任何人都有信心可以做得很出色。但如果,你整天被乱七八糟的事所累,还有心思和精力把每件事都作好么?
fig-1.JPG
     “单一职责”就是要在设计中为每种职责设计一个类,彼此保持正交,互不干涉。这个雕塑(二重奏)就是正交的一个例子,钢琴家和小提琴家各自演奏自己的乐谱,而结果就是一个和谐的交响乐。当然,真实世界中,演奏小提琴和弹钢琴的必须是两个人,但是在软件中,我们往往会把两者甚至更多搅和到一起,很多时候只是为了方便或是最初设计的时候没有想到。 

      这样的例子在设计中很常见,书中就给了一个很好的例子:调制解调器。这是一个调制解调器最基本的功能。但是这个类事实上完成了两个职责:连接的建立和中断、数据的发送和接收。显然,这违反了SRP。这样做会有潜在的问题:当仅需要改变数据连接方式时,必须修改Modem类,而修改Modem类的结果就是使得任何依赖Modem类的元素都需要重新编译,不管它是不是用到了数据连接功能。解决的办法,书中也已经给出:重构Modem类,从中抽出两个接口,一个专门负责连接、另一个专门负责数据发送。依赖Modem类的元素也要做相应的细化,根据职责的不同分别依赖不同的接口。最后由ModemImplementation类实现这两个接口。
fig-2.JPG

      从这个例子中,我们不难发现,违反SRP通常是由于过于“真实”地设计了一个类所造成的。因此,解决办法是往更高一层进行抽象化提取,将对某个具体类的依赖改变为对一组接口或抽象类的依赖。当然,这个抽象化的提取应该根据需要设计,而不是盲目提取。比如刚才这个Modem的例子中,如果有必要,还可以把DataChannel抽象为DataSender和DataReceiver两个接口。
 

posted @ 2006-01-09 21:17 GHawk 阅读(5556) | 评论 (5)编辑 收藏

敏捷软件开发 读书笔记 (1)——设计的目标

软件设计是一种抽象活动,设计所要实现的是产出代码。就这一点来说,任何人都会设计。但是,正如我们日常生活中所耳闻目睹或亲身经历,设计有优劣之分。

从项目管理的角度去理解,设计是为了满足涉众(Stakeholders)的需求。显然,一个设计应该满足客户对系统的功能及非功能需求。但单是满足了这一点,并不能称为一个好的设计。因为开发者同样属于涉众!而开发者的需求又是怎样的呢?至少,应该有以下几条吧:

  • 老板希望软件交付后,不应该有很高的维护成本。如果开发人员为了维护而经常出差或者加班且久久不能投入新项目,显然,换了谁是老板都不愿意这种事情发生。
  • 开发人员呢?谁愿意放弃和家人朋友而拼死拼活在单位加班,总是有这么多麻烦事缠着你,烦不烦哪!
  • ……等等

所以,设计应该尽可能多地照顾到维护和变更。

为了兼顾各户满意和维护成本,设计应该不断挑战其终极目标——松耦合。不管是XP或UP,这个目标都不会改变。OO设计中的五大原则,其根本目的就是降低组件间的耦合度,避免牵一发则动全身的现象发生。降低耦合度不仅能够提高软件内在的质量,还能大大减少不必要的编译时间、减少向版本控制系统提交源码的网络开销……

如何鉴别设计的这一指标?软件工程中有专用的度量:CBO(Coupling Between Objects),那是由公式计算出来的,也有很多工具支持,值得一试。(听过几次李维先生的讲座,他经常拿Together的度量功能炫耀^_^)

但是,作为一个开发人员,对手中的代码应该有适当的敏感性。毕竟,这些代码是你亲手创造的,谁不希望自己的作品得到众人的赞许?或许能换得一次加薪升职的机会^_^ 退一步,这可关系到宝贵的休息时间啊。所以,开发者应该对自己的产品有这样一种意识:及时修正设计中不合理的地方。

敏捷过程告诉我们:在代码“有味道”的时候进行重构。“有味道”是代码正在变质的标志,很遗憾,能够使代码保持原味的防腐剂还没发明。为了保证代码质量,及时重构是必要的。这就像在烧烤的时候为了防止烤焦,你得坐在炉子前经常翻动肉块一样。

如何闻出代码的味道?认真学习一下OO吧,别以为OO很简单,就是继承+封装+多态,谁都会。即使是书中记述的五大原则,想要运用自如,也得多感觉感觉才行。很多时候,我们不知不觉就把蛆虫放进了代码中……

好了,下一篇:OO五大原则

posted @ 2006-01-06 18:17 GHawk 阅读(1592) | 评论 (3)编辑 收藏

敏捷软件开发 读书笔记 (序——废话)

7-5083-1503-0l.gif

最近正在读这本书,喜欢影印版,是因为书中漂亮的插图。:)惭愧,如此的好书到现在才去读。
准备边读边记录些心得,今天先说些废话。:P

先粗略地概览了一遍全书。本书主要分以下几个部分:

  1. 敏捷软件过程。主要以XP为例。这部分的最后一章,用一个对话式的小故事讲述了一个非常小的过程。给了读者关于敏捷过程的形象化的认识。
  2. 敏捷设计。这部分是个很大的看点。它讲述了设计中一些常见的问题,及其应对(用几个经典的设计原则)。
  3. 案例实践。讲述了如何利用设计模式去实践第二部分中提到的设计原则和避免设计中的“味道”。

之所以觉得这本书好,还与一个人有关。就是交大软件学院的林德彰老师。林先生的课,风趣幽默,能够用直观形象的语言让学生对讲课内容产生深刻的印象。(我可不是托儿,网上能搜到些林先生讲课的片断,要是怀疑,可以验证一番)。记得在软件工程这门课里,林先生给我们讲了很多有关设计原则的内容,其中就有“开闭原则(OCP)”、“里氏替换原则(LSP)”等……就把这本书当作是一本补充读物吧。

言归正传。个人感觉这本书的总体风格,就和所要讲的“敏捷”一样,并不带着厚重的学院派风味,而是更注重实践。并不是没有理论,只是把理论融入到了实践中,简化了理论的复杂性。读起来感觉很带劲儿。

废话说到这里,下一步的计划就是跟着自己的进度写读书心得了。我想把对书中内容的理解和以前在林先生的课上所学的结合在一起,导出阅读此书时的大脑活动镜像。

posted @ 2005-12-27 12:00 GHawk 阅读(1610) | 评论 (4)编辑 收藏

一个介绍Java开源项目及工具的网站

按功能进行了归类
http://java-source.net


再追加一个中文的
http://www.open-open.com

posted @ 2005-12-20 13:45 GHawk 阅读(509) | 评论 (1)编辑 收藏

用 Cobertura 测量测试覆盖率

http://www-128.ibm.com/developerworks/cn/java/j-cobertura/

用 Cobertura 测量测试覆盖率

找出隐藏 bug 的未测试到的代码

developerWorks
文档选项
将此页作为电子邮件发送

将此页作为电子邮件发送

未显示需要 JavaScript 的文档选项


对此页的评价

帮助我们改进这些内容


级别: 初级

Elliotte Rusty Harold, 副教授, Polytechnic University

2005 年 5 月 26 日

Cobertura 是一种开源工具,它通过检测基本的代码,并观察在测试包运行时执行了哪些代码和没有执行哪些代码,来测量测试覆盖率。除了找出未测试到的代码并发现 bug 外,Cobertura 还可以通过标记无用的、执行不到的代码来优化代码,还可以提供 API 实际操作的内部信息。Elliotte Rusty Harold 将与您分享如何利用代码覆盖率的最佳实践来使用 Cobertura。

尽管测试先行编程(test-first programming)和单元测试已不能算是新概念,但测试驱动的开发仍然是过去 10 年中最重要的编程创新。最好的一些编程人员在过去半个世纪中一直在使用这些技术,不过,只是在最近几年,这些技术才被广泛地视为在时间及成本预算内开发健壮的无缺陷软件的关键所在。但是,测试驱动的开发不能超过测试所能达到的程度。测试改进了代码质量,但这也只是针对实际测试到的那部分代码而言的。您需要有一个工具告诉您程序的哪些部分没有测试到,这样就可以针对这些部分编写测试代码并找出更多 bug。

Mark Doliner 的 Cobertura (cobertura 在西班牙语是覆盖的意思)是完成这项任务的一个免费 GPL 工具。Cobertura 通过用额外的语句记录在执行测试包时,哪些行被测试到、哪些行没有被测试到,通过这种方式来度量字节码,以便对测试进行监视。然后它生成一个 HTML 或者 XML 格式的报告,指出代码中的哪些包、哪些类、哪些方法和哪些行没有测试到。可以针对这些特定的区域编写更多的测试代码,以发现所有隐藏的 bug。

阅读 Cobertura 输出

我们首先查看生成的 Cobertura 输出。图 1 显示了对 Jaxen 测试包运行 Cobertura 生成的报告(请参阅 参考资料)。从该报告中,可以看到从很好(在 org.jaxen.expr.iter 包中几乎是 100%)到极差(在 org.jaxen.dom.html 中完全没有覆盖)的覆盖率结果。


图 1. Jaxen 的包级别覆盖率统计数据

Cobertura 通过被测试的行数和被测试的分支数来计算覆盖率。第一次测试时,两种测试方法之间的差别并不是很重要。Cobertura 还为类计算平均 McCabe 复杂度(请参阅 参考资料)。

可以深入挖掘 HTML 报告,了解特定包或者类的覆盖率。图 2 显示了 org.jaxen.function 包的覆盖率统计。在这个包中,覆盖率的范围从 SumFunction 类的 100% 到 IdFunction 类的仅为 5%。


图 2. org.jaxen.function 包中的代码覆盖率

进一步深入到单独的类中,具体查看哪一行代码没有测试到。图 3 显示了 NameFunction 类中的部分覆盖率。最左边一栏显示行号。后一栏显示了执行测试时这一行被执行的次数。可以看出,第 112 行被执行了 100 次,第 114 行被执行了 28 次。用红色突出显示的那些行则根本没有测试到。这个报告表明,虽然从总体上说该方法被测试到了,但实际上还有许多分支没有测试到。


图 3. NameFunction 类中的代码覆盖率

Cobertura 是 jcoverage 的分支(请参阅 参考资料)。GPL 版本的 jcoverage 已经有一年没有更新过了,并且有一些长期存在的 bug,Cobertura 修复了这些 bug。原来的那些 jcoverage 开发人员不再继续开发开放源码,他们转向开发 jcoverage 的商业版和 jcoverage+,jcoverage+ 是一个从同一代码基础中发展出来的封闭源代码产品。开放源码的奇妙之处在于:一个产品不会因为原开发人员决定让他们的工作获得相应的报酬而消亡。

确认遗漏的测试

利用 Cobertura 报告,可以找出代码中未测试的部分并针对它们编写测试。例如,图 3 显示 Jaxen 需要进行一些测试,运用 name() 函数对文字节点、注释节点、处理指令节点、属性节点和名称空间节点进行测试。

如果有许多未覆盖的代码,像 Cobertura 在这里报告的那样,那么添加所有缺少的测试将会非常耗时,但也是值得的。不一定要一次完成它。您可以从被测试的最少的代码开始,比如那些所有没有覆盖的包。在测试所有的包之后,就可以对每一个显示为没有覆盖的类编写一些测试代码。对所有类进行专门测试后,还要为所有未覆盖的方法编写测试代码。在测试所有方法之后,就可以开始分析对未测试的语句进行测试的必要性。

(几乎)不留下任何未测试的代码

是否有一些可以测试但不应测试的内容?这取决于您问的是谁。在 JUnit FAQ 中,J. B. Rainsberger 写到“一般的看法是:如果 自身 不会出问题,那么它会因为太简单而不会出问题。第一个例子是 getX() 方法。假定 getX() 方法只提供某一实例变量的值。在这种情况下,除非编译器或者解释器出了问题,否则 getX() 是不会出问题的。因此,不用测试 getX(),测试它不会带来任何好处。对于 setX() 方法来说也是如此,不过,如果 setX() 方法确实要进行任何参数验证,或者说确实有副作用,那么还是有必要对其进行测试。”

理论上,对未覆盖的代码编写测试代码不一定就会发现 bug。但在实践中,我从来没有碰到没有发现 bug 的情况。未测试的代码充满了 bug。所做的测试越少,在代码中隐藏的、未发现的 bug 就会越多。

我不同意。我已经记不清在“简单得不会出问题”的代码中发现的 bug 的数量了。确实,一些 getter 和 setter 很简单,不可能出问题。但是我从来就没有办法区分哪些方法是真的简单得不会出错,哪些方法只是看上去如此。编写覆盖像 setter 和 getter 这样简单方法的测试代码并不难。为此所花的少量时间会因为在这些方法中发现未曾预料到的 bug 而得到补偿。

一般来说,开始测量后,达到 90% 的测试覆盖率是很容易的。将覆盖率提高到 95% 或者更高就需要动一下脑筋。例如,可能需要装载不同版本的支持库,以测试没有在所有版本的库中出现的 bug。或者需要重新构建代码,以便测试通常执行不到的部分代码。可以对类进行扩展,让它们的受保护方法变为公共方法,这样就可以对这些方法进行测试。这些技巧看起来像是多此一举,但是它们曾帮助我在一半的时间内发现更多的未发现的 bug。

并不总是可以得到完美的、100% 的代码覆盖率。有时您会发现,不管对代码如何改造,仍然有一些行、方法、甚至是整个类是测试不到的。下面是您可能会遇到的挑战的一些例子:

  • 只在特定平台上执行的代码。例如,在一个设计良好的 GUI 应用程序中,添加一个 Exit 菜单项的代码可以在 Windows PC 上运行,但它不能在 Mac 机上运行。

  • 捕获不会发生的异常的 catch 语句,比如在从 ByteArrayInputStream 进行读取操作时抛出的 IOException

  • 非公共类中的一些方法,它们永远也不会被实际调用,只是为了满足某个接口契约而必须实现。

  • 处理虚拟机 bug 的代码块,比如说,不能识别 UTF-8 编码。

考虑到上面这些以及类似的情况,我认为一些极限程序员自动删除所有未测试代码的做法是不切实际的,并且可能具有一定的讽刺性。不能总是获得绝对完美的测试覆盖率并不意味着就不会有更好的覆盖率。

然而,比执行不到的语句和方法更常见的是残留代码,它不再有任何作用,并且从代码基中去掉这些代码也不会产生任何影响。有时可以通过使用反射来访问私有成员这样的怪招来测试未测试的代码。还可以为未测试的、包保护(package-protected)的代码来编写测试代码,将测试类放到将要测试的类所在那个包中。但最好不要这样做。所有不能通过发布的(公共的和受保护的)接口访问的代码都应删除。执行不到的代码不应当成为代码基的一部分。代码基越小,它就越容易被理解和维护。

不要漏掉测量单元测试包和类本身。我不止一次注意到,某些个测试方法或者类没有被测试包真正运行。通常这表明名称规范中存在问题(比如将一个方法命名为 tesSomeReallyComplexCondition,而不是将其命名为 testSomeReallyComplexCondition),或者忘记将一个类添加到主 suite() 方法中。在其他情况下,未预期的条件导致跳过了测试方法中的代码。不管是什么情况,都是虽然已经编写了测试代码,但没有真正运行它。JUnit 不会告诉您它没有像您所想的那样运行所有测试,但是 Cobertura 会告诉您。找出了未运行的测试后,改正它一般很容易。



回页首


运行 Cobertura

在了解了测量代码覆盖率的好处后,让我们再来讨论一下如何用 Cobertura 测量代码覆盖率的具体细节。Cobertura 被设计成为在 Ant 中运行。现在还没有这方面的 IDE 插件可用,不过一两年内也许就会有了。

首先需要在 build.xml 文件中添加一个任务定义。以下这个顶级 taskdef 元素将 cobertura.jar 文件限定在当前工作目录中:

<taskdef classpath="cobertura.jar" resource="tasks.properties" />

然后,需要一个 cobertura-instrument 任务,该任务将在已经编译好的类文件中添加日志代码。todir 属性指定将测量类放到什么地方。fileset 子元素指定测量哪些 .class 文件:

<target name="instrument">
  <cobertura-instrument todir="target/instrumented-classes">
    <fileset dir="target/classes">
      <include name="**/*.class"/>
    </fileset>
  </cobertura-instrument>
</target>

用通常运行测试包的同一种类型的 Ant 任务运行测试。惟一的区别在于:被测量的类必须在原始类出现在类路径中之前出现在类路径中,而且需要将 Cobertura JAR 文件添加到类路径中:

<target name="cover-test" depends="instrument">
  <mkdir dir="${testreportdir}" />
  <junit dir="./" failureproperty="test.failure" printSummary="yes" 
         fork="true" haltonerror="true">
    <!-- Normally you can create this task by copying your existing JUnit
         target, changing its name, and adding these next two lines.
         You may need to change the locations to point to wherever 
         you've put the cobertura.jar file and the instrumented classes. -->
    <classpath location="cobertura.jar"/>
    <classpath location="target/instrumented-classes"/>
    <classpath>
      <fileset dir="${libdir}">
        <include name="*.jar" />
      </fileset>
      <pathelement path="${testclassesdir}" />
      <pathelement path="${classesdir}" />
    </classpath>
    <batchtest todir="${testreportdir}">
      <fileset dir="src/java/test">
        <include name="**/*Test.java" />
        <include name="org/jaxen/javabean/*Test.java" />
      </fileset>
    </batchtest>
  </junit>
</target>>

Jaxen 项目使用 JUnit 作为其测试框架,但是 Cobertura 是不受框架影响的。它在 TestNG、Artima SuiteRunner、HTTPUni 或者在您自己在地下室开发的系统中一样工作得很好。

最后,cobertura-report 任务生成本文开始部分看到的那个 HTML 文件:

<target name="coverage-report" depends="cover-test">
 <cobertura-report srcdir="src/java/main" destdir="cobertura"/>
</target>

srcdir 属性指定原始的 .java 源代码在什么地方。destdir 属性指定 Cobertura 放置输出 HTML 的那个目录的名称。

在自己的 Ant 编译文件中加入了类似的任务后,就可以通过键入以下命令来生成一个覆盖报告:

% ant instrument
% ant cover-test
% ant coverage-report

当然,如果您愿意的话,还可以改变目标任务的名称,或者将这三项任务合并为一个目标任务。



回页首


结束语

Cobertura 是敏捷程序员工具箱中新增的一个重要工具。通过生成代码覆盖率的具体数值,Cobertura 将单元测试从一种艺术转变为一门科学。它可以寻找测试覆盖中的空隙,直接找到 bug。测量代码覆盖率使您可以获得寻找并修复 bug 所需的信息,从而开发出对每个人来说都更健壮的软件。



回页首


参考资料



回页首


关于作者

Elliotte Rusty Harold 出生在新奥尔良,现在他还定期回老家喝一碗美味的秋葵汤。不过目前,他和妻子 Beth 定居在纽约临近布鲁克林的 Prospect Heights,同住的还有他的猫咪 Charm(取自夸克)和 Marjorie(取自他岳母的名字)。他是 Polytechnic 大学计算机科学的副教授,讲授 Java 技术和面向对象编程。他的 Cafe au Lait 网站是 Internet 上最受欢迎的独立 Java 站点之一,姊妹站点 Cafe con Leche 已经成为最受欢迎的 XML 站点之一。他的著作包括 Effective XML Processing XML with Java Java Network Programming The XML 1.1 Bible 。目前他正在从事 XML 的 XOM API、Jaxen XPath 引擎和 Jester 测试覆盖率工具的开发工作。


posted @ 2005-12-17 11:23 GHawk 阅读(423) | 评论 (0)编辑 收藏

EasyMock 2.0_ReleaseCandidate 文档翻译

     摘要: EasyMock 2.0_ReleaseCandidate Readme Documentation for release 2.0_ReleaseCandidate (October 15 2005)© 2001-2005 OFFIS, Tammo Freese. 翻译:GHawk, 2005-12-15 EasyMock 2 is a library that provides an ...  阅读全文

posted @ 2005-12-15 16:06 GHawk 阅读(4760) | 评论 (2)编辑 收藏

过程的代价

这个月刚进入公司,加入了一个10人左右的团队,用Java做一个网站后台。

客户是日本公司,他们做了项目的大部分分析(Requirements, Use cases, Domain model...)。我们负责的是详细设计和开发。我是项目开始几星期后才进的公司。Schedule也已经为我分配好了。大家都按照schedule上的安排工作着。

上星期开会的时候得知,日本这次采用的是agile过程。而我们的schedule更类似于RUP这样的过程。RUP这个学院派和Agile这个造反派狭路相逢,问题也就出现了。

大家工作都很卖力,为了能按进度提交制品,有时还通宵达旦解决问题。我们这支团队的战斗力和信心是不容怀疑的。可是大家努力的结果换来的却是用户的抱怨。大家都困惑不解。问题究竟出在哪儿?

日方在项目中强调的是Agile过程,我们采用的则是传统的过程。一开始,两个过程方法之间的差异并不大;对我们提交的制品,客户也没有什么异议。但是,直到客户提出问题之前,我们所提交的制品都是一些设计文档。而我们的制品也仅限于此——没有一个可用的EAR包、没有写过 test case。很明显,我们犯了agile的大忌。

Agile所强调的是快速的构建、轻量级的迭代、TDD等。由于之前没有写test case,详细设计也没有test case可以参照。设计本身是不是合理,是不是testable也不得而知。致使在设计test case的时候无从下手,很多类甚至都没有办法测试。整个架构的可行性很难估算。

往后考虑。一次大规模的重构可能是少不了的。虽然agile过程本身提倡以TDD为基础的重构。但是现在的重构可能造成的代价已经不是一次轻量级的增量迭代了。

说到这里,总结几点,希望能在以后的工作中引起注意:
1. Agile很难管理,项目早期应该对各种风险有尽可能全面的评估,schedule的设置中应该定义好 test case 和 build 的时间点。
2. 设计不必太详细,用频繁的测试和重构完善设计。
3. Test case 优先设计,这样在架构中就会对testability有足够多的考虑。
4. 团队内部对共同的难题应该及早进行讨论和解决,问题的解决方案应该传递到每个组员,尽可能保证团队的能力同步。

posted @ 2005-12-14 09:57 GHawk 阅读(1198) | 评论 (5)编辑 收藏

仅列出标题
共3页: 上一页 1 2 3 下一页