#
开闭原则很简单,一句话:“Closed for Modification; Open for Extension”——“对变更关闭;对扩展开放”。开闭原则其实没什么好讲的,我将其归结为一个高层次的设计总则。就这一点来讲,OCP的地位应该比SRP优先。
OCP的动机很简单:软件是变化的。不论是优质的设计还是低劣的设计都无法回避这一问题。OCP说明了软件设计应该尽可能地使架构稳定而又容易满足不同的需求。
为什么要OCP?答案也很简单——重用。
“重用”,并不是什么软件工程的专业词汇,它是工程界所共用的词汇。早在软件出现前,工程师们就在实践“重用”了。比如机械产品,通过零部件的组装得到最终的能够使用的工具。由于机械部件的设计和制造过程是极其复杂的,所以互换性是一个重要的特性。一辆车可以用不同的发动机、不同的变速箱、不同的轮胎……很多东西我们直接买来装上就可以了。这也是一个OCP的例子。(可能是由于我是搞机械出身的吧,所以就举些机械方面的例子^_^)。
如何在OO中引入OCP原则?把对实体的依赖改为对抽象的依赖就行了。下面的例子说明了这个过程:
05赛季的时候,一辆F1赛车有一台V10引擎。但是到了06赛季,国际汽联修改了规则,一辆F1赛车只能安装一台V8引擎。车队很快投入了新赛车的研发,不幸的是,从工程师那里得到消息,旧车身的设计不能够装进新研发的引擎。我们不得不为新的引擎重新打造车身,于是一辆新的赛车诞生了。但是,麻烦的事接踵而来,国际汽联频频修改规则,搞得设计师在“赛车”上改了又改,最终变得不成样子,只能把它废弃。
为了能够重用这辆昂贵的赛车,工程师们提出了解决方案:首先,在车身的设计上预留出安装引擎的位置和管线。然后,根据这些设计好的规范设计引擎(或是引擎的适配器)。于是,新的赛车设计方案就这样诞生了。
显然,通过重构,这里应用的是一个典型的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设计的精髓。)
from http://blog.csdn.net/wangyihust/archive/2006/01/02/568616.aspx
JUnit是由 Erich Gamma 和 Kent Beck 编写的一个回归测试框架(regression testing framework)。Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。
http://www.junit.org/
Cactus是一个基于JUnit框架的简单测试框架,用来单元测试服务端Java代码。Cactus框架的主要目标是能够单元测试服务端的使用Servlet对象的Java方法如HttpServletRequest,HttpServletResponse,HttpSession等。
http://jakarta.apache.org/cactus/
Abbot是一个用来测试Java GUIs的框架。用简单的基于XML的脚本或者Java代码,你就可以开始一个GUI。
http://abbot.sourceforge.net/
Junitperf实际是junit的一个decorator,通过编写用于junitperf的单元测试,我们也可使测试过程自动化。
http://www.clarkware.com/software/JUnitPerf.html
DbUnit是为数据库驱动的项目提供的一个对JUnit 的扩展,除了提供一些常用功能,它可以将你的数据库置于一个测试轮回之间的状态。
http://dbunit.sourceforge.net/
Mockrunner用在J2EE环境中进行应用程序的单元测试。它不仅支持Struts actions, servlets,过滤器和标签类还包括一个JDBC和一个JMS测试框架,可以用于测试基于EJB的应用程序。
http://mockrunner.sourceforge.net/index.html
DBMonster是一个用生成随机数据来测试SQL数据库的压力测试工具。
http://dbmonster.kernelpanic.pl/
MockEJB是一个不需要EJB容器就能运行EJB并进行测试的轻量级框架。
http://mockejb.sourceforge.net/
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使得你能够为Java偏移应用程序编写测试例子。它为从用代码打开的窗口上获得句柄提供了支持;为在一个部件层次定位部件提供支持;为在部件中发起事件(例如按一个按钮)以及以线程安全方式处理部件测试提供支持。
http://jfcunit.sourceforge.net/
JTestCase 使用XML文件来组织多测试案例数据,声明条件(操作和期望的结果),提供了一套易于使用的方法来检索XML中的测试案例,按照数据文件的定义来声明结果。
http://jtestcase.sourceforge.net/
SQLUnit是一个单元测试框架,用于对数据库存储过程进行回归测试。用 Java/JUnit/XML开发。
http://sqlunit.sourceforge.net
JTR (Java Test Runner)是一个开源的基于反转控制(IOC)的J2EE测试框架。它允许你构建复杂的J2EE测试套件(Test Suites)并连到应用服务器执行测试,可以包括多个测试实例。JTR的licensed是GPL协议。
http://jtrunner.sourceforge.net/
Marathon是一个针对使用Java/Swing开发GUI应用程序的测试框架,它由recorder, runner 和 editor组成,测试脚本是python代码。Marathon的焦点是放在最终用户的测试上。
http://marathonman.sourceforge.net
TestNG是根据JUnit 和 NUnit思想而构建的一个测试框架,但是TestNG增加了许多新的功能使得它变得更加强大与容易使用比如:
*支持JSR 175注释(JDK 1.4利用JavaDoc注释同样也支持)
*灵活的Test配置
*支持默认的runtime和logging JDK功能
*强大的执行模型(不再TestSuite)
*支持独立的测试方法。
http://testng.org/
Surrogate Test framework是一个值得称赞单元测试框架,特别适合于大型,复杂Java系统的单元测试。这个框架能与JUnit,MockEJB和各种支持模拟对象(mock object )的测试工具无缝给合。这个框架基于AspectJ技术。
http://surrogate.sourceforge.net
MockCreator可以为给定的interface或class生成模拟对象(Mock object)的源码。
http://mockcreator.sourceforge.net/
jMock利用mock objects思想来对Java code进行测试。jMock具有以下特点:容易扩展,让你快速简单地定义mock objects,因此不必打破程序间的关联,让你定义灵活的超越对象之间交互作用而带来测试局限,减少你测试地脆弱性。
http://www.jmock.org/
EasyMock为Mock Objects提供接口并在JUnit测试中利用Java的proxy设计模式生成它们的实例。EasyMock最适合于测试驱动开发。
http://www.easymock.org/
The Grinder是一个负载测试框架。在BSD开源协议下免费使用。
http://grinder.sourceforge.net/
XMLUnit不仅有Java版本的还有.Net版本的。Java开发的XMLUnit提供了两个JUnit 扩展类XMLAssert和XMLTestCase,和一组支持的类。这些类可以用来比较两张XML之间的不同之处,展示XML利用XSLT来,校验XML,求得XPath表达式在XML中的值,遍历XML中的某一节点利DOM展开。
http://xmlunit.sourceforge.net/
Jameleon一个自动化测试工具。它被用来测试各种各样的应用程序,所以它被设计成插件模式。为了使整个测试过程变得简单Jameleon提供了一个GUI,因此Jameleon实现了一个Swing 插件。
http://jameleon.sourceforge.net/index.html
J2MEUnit是应用在J2ME应用程序的一个单元测试框架。它基于JUnit。
http://j2meunit.sourceforge.net/
Jetif是一个用纯Java实现的回归测试框架。它为Java程序单元测试以及功能测试提供了一个简单而且可 伸缩的架构,可以用于个人开发或企业级开发的测试。它容易使用,功能强大,而且拥有一些企业级测试的重要功能。Jetif来源于JUnit, JTestCase以及TestNG的启发,有几个基本的概念直接来自于JUnit, 比如说断言机制,Test Listener的概念,因此从JUnit转到Jetif是非常容易的。
http://jetif.sourceforge.net/
GroboUtils使得扩展Java测试变得可能。它包括用在Java不同方面测试的多个子项目。在GroboUtils中最常被到的工具是:多线程测试(multi-threaded tests),整体单元测试(hierarchial unit tests),代码覆盖工具(code coverage tool)。
http://groboutils.sourceforge.net/
TESTARE是用来简化分布式应用程序(比如:在SERVLETS,JMS listeners, CORBA ORBs或RMI环境下)测试开发过程的一个测试框架。
https://testare.dev.java.net/
一点说明:OO的五大原则是指SRP、OCP、LSP、DIP、ISP。这五个原则是书中所提到的。除此之外,书中还提到一些高层次的原则用于组织高层的设计元素,这些放到下次再写。当然,OO设计的原则可能不止这五个,希望大家多提宝贵意见,多多交流。
在学习和使用OO设计的时候,我们应该明白:OO的出现使得软件工程师们能够用更接近真实世界的方法描述软件系统。然而,软件毕竟是建立在抽象层次上的东西,再怎么接近真实,也不能替代真实或被真实替代。
OO设计的五大原则之间并不是相互孤立的。彼此间存在着一定关联,一个可以是另一个原则的加强或是基础。违反其中的某一个,可能同时违反了其余的原则。因此应该把这些原则融会贯通,牢记在心!
1. SRP(Single Responsibility Principle 单一职责原则)
单一职责很容易理解,也很容易实现。所谓单一职责,就是一个设计元素只做一件事。什么是“只做一件事”?简单说就是少管闲事。现实中就是如此,如果要你专心做一件事情,任何人都有信心可以做得很出色。但如果,你整天被乱七八糟的事所累,还有心思和精力把每件事都作好么?

“单一职责”就是要在设计中为每种职责设计一个类,彼此保持正交,互不干涉。这个雕塑(二重奏)就是正交的一个例子,钢琴家和小提琴家各自演奏自己的乐谱,而结果就是一个和谐的交响乐。当然,真实世界中,演奏小提琴和弹钢琴的必须是两个人,但是在软件中,我们往往会把两者甚至更多搅和到一起,很多时候只是为了方便或是最初设计的时候没有想到。
这样的例子在设计中很常见,书中就给了一个很好的例子:调制解调器。这是一个调制解调器最基本的功能。但是这个类事实上完成了两个职责:连接的建立和中断、数据的发送和接收。显然,这违反了SRP。这样做会有潜在的问题:当仅需要改变数据连接方式时,必须修改Modem类,而修改Modem类的结果就是使得任何依赖Modem类的元素都需要重新编译,不管它是不是用到了数据连接功能。解决的办法,书中也已经给出:重构Modem类,从中抽出两个接口,一个专门负责连接、另一个专门负责数据发送。依赖Modem类的元素也要做相应的细化,根据职责的不同分别依赖不同的接口。最后由ModemImplementation类实现这两个接口。
从这个例子中,我们不难发现,违反SRP通常是由于过于“真实”地设计了一个类所造成的。因此,解决办法是往更高一层进行抽象化提取,将对某个具体类的依赖改变为对一组接口或抽象类的依赖。当然,这个抽象化的提取应该根据需要设计,而不是盲目提取。比如刚才这个Modem的例子中,如果有必要,还可以把DataChannel抽象为DataSender和DataReceiver两个接口。
软件设计是一种抽象活动,设计所要实现的是产出代码。就这一点来说,任何人都会设计。但是,正如我们日常生活中所耳闻目睹或亲身经历,设计有优劣之分。
从项目管理的角度去理解,设计是为了满足涉众(Stakeholders)的需求。显然,一个设计应该满足客户对系统的功能及非功能需求。但单是满足了这一点,并不能称为一个好的设计。因为开发者同样属于涉众!而开发者的需求又是怎样的呢?至少,应该有以下几条吧:
- 老板希望软件交付后,不应该有很高的维护成本。如果开发人员为了维护而经常出差或者加班且久久不能投入新项目,显然,换了谁是老板都不愿意这种事情发生。
- 开发人员呢?谁愿意放弃和家人朋友而拼死拼活在单位加班,总是有这么多麻烦事缠着你,烦不烦哪!
- ……等等
所以,设计应该尽可能多地照顾到维护和变更。
为了兼顾各户满意和维护成本,设计应该不断挑战其终极目标——松耦合。不管是XP或UP,这个目标都不会改变。OO设计中的五大原则,其根本目的就是降低组件间的耦合度,避免牵一发则动全身的现象发生。降低耦合度不仅能够提高软件内在的质量,还能大大减少不必要的编译时间、减少向版本控制系统提交源码的网络开销……
如何鉴别设计的这一指标?软件工程中有专用的度量:CBO(Coupling Between Objects),那是由公式计算出来的,也有很多工具支持,值得一试。(听过几次李维先生的讲座,他经常拿Together的度量功能炫耀^_^)
但是,作为一个开发人员,对手中的代码应该有适当的敏感性。毕竟,这些代码是你亲手创造的,谁不希望自己的作品得到众人的赞许?或许能换得一次加薪升职的机会^_^ 退一步,这可关系到宝贵的休息时间啊。所以,开发者应该对自己的产品有这样一种意识:及时修正设计中不合理的地方。
敏捷过程告诉我们:在代码“有味道”的时候进行重构。“有味道”是代码正在变质的标志,很遗憾,能够使代码保持原味的防腐剂还没发明。为了保证代码质量,及时重构是必要的。这就像在烧烤的时候为了防止烤焦,你得坐在炉子前经常翻动肉块一样。
如何闻出代码的味道?认真学习一下OO吧,别以为OO很简单,就是继承+封装+多态,谁都会。即使是书中记述的五大原则,想要运用自如,也得多感觉感觉才行。很多时候,我们不知不觉就把蛆虫放进了代码中……
好了,下一篇:OO五大原则
最近正在读这本书,喜欢影印版,是因为书中漂亮的插图。:)惭愧,如此的好书到现在才去读。
准备边读边记录些心得,今天先说些废话。:P
先粗略地概览了一遍全书。本书主要分以下几个部分:
- 敏捷软件过程。主要以XP为例。这部分的最后一章,用一个对话式的小故事讲述了一个非常小的过程。给了读者关于敏捷过程的形象化的认识。
- 敏捷设计。这部分是个很大的看点。它讲述了设计中一些常见的问题,及其应对(用几个经典的设计原则)。
- 案例实践。讲述了如何利用设计模式去实践第二部分中提到的设计原则和避免设计中的“味道”。
之所以觉得这本书好,还与一个人有关。就是交大软件学院的林德彰老师。林先生的课,风趣幽默,能够用直观形象的语言让学生对讲课内容产生深刻的印象。(我可不是托儿,网上能搜到些林先生讲课的片断,要是怀疑,可以验证一番)。记得在软件工程这门课里,林先生给我们讲了很多有关设计原则的内容,其中就有“开闭原则(OCP)”、“里氏替换原则(LSP)”等……就把这本书当作是一本补充读物吧。
言归正传。个人感觉这本书的总体风格,就和所要讲的“敏捷”一样,并不带着厚重的学院派风味,而是更注重实践。并不是没有理论,只是把理论融入到了实践中,简化了理论的复杂性。读起来感觉很带劲儿。
废话说到这里,下一步的计划就是跟着自己的进度写读书心得了。我想把对书中内容的理解和以前在林先生的课上所学的结合在一起,导出阅读此书时的大脑活动镜像。
用 Cobertura 测量测试覆盖率
找出隐藏 bug 的未测试到的代码  |
 |
 |
级别: 初级
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 所需的信息,从而开发出对每个人来说都更健壮的软件。
参考资料
 |
关于作者
|
摘要: 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 ...
阅读全文
这个月刚进入公司,加入了一个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. 团队内部对共同的难题应该及早进行讨论和解决,问题的解决方案应该传递到每个组员,尽可能保证团队的能力同步。