单元测试(JUnit)的应用一.概要单元测试不仅仅是保证代码在方法级别的正确性,它还能改进设计,易于对代码重构。凡是容易编写单元测试的代码,往往是优秀的设计和松耦合的组件,凡是难于编写单元测试的代码,往往是设计不佳和耦合度高的系统,因此,编写单元测试不仅仅是掌握单元测试柜架的用法,更重要的是在编写单元测试的过程中发现设计缺陷,改进系统结构,从而实现良好的可扩展性。任何一个项目,单元测试应该在详细设计之后开始进行,首先根据详细设计文档进行单元测试用例的编写,编写完成后进行代码开发,代码完成后运行单元测试,如果通过,则该方法可以发布运行,如果不通过需要进行代码改造,再进行单元测试,直到单元测试运行通过为止。每新增一个功能时的开发流程如图1所示:编写测试用例编写代码完成对代码做小的修改运行测试通过失败图1本文首先按照上图所示流程进行一个简单功能的开发,同时使用JUnit4提供的各种功能开展有效的单元测试,接着对JUnit技术进行了简单的介绍。二.软件支持Eclipse:最为流行的IDE,它全面集成了JUnit,并从版本3.2开始支持JUnit4。当然JUnit并不依赖于任何IDE。您可以从上下载最新的Eclipse版本。Ant:基于Java的开源构建工具,您可以在上得到最新的版本和丰富的文档。Eclipse中已经集成了Ant。JUnit:它的官方网站是。您可以从上面获取关于JUnit的最新消息。如果您和本文一样在Eclipse中使用JUnit,就不必再下载了。三.JUnit的实际应用例子3.1功能需求将Java对象名称(每个单词的头字母大写)按照数据库命名的习惯进行格式化,格式化后的数据为小写字母,并且使用下划线分割命名单词,要求对输入数据进行非法验证。3.2编写测试首先新建一个Java工程——TestJUnit。打开项目TestJUnit的属性页-选择“JavaBuildPath”子选项-点选“AddLibrary…”按钮-在弹出的“AddLibrary”对话框中选择JUnit(图2),并在下一页中选择版本4.1后点击“Finish”按钮。图2请注意JDK的版本:JUnit4.1是基于Java5的升级版本,它使用了Tiger中的很多新特性来简化原有的使用方式。正因为如此,它并不能直接运行在JDK1.4.x版本上。如果您需要在JDK1.4.x版本使用JUnit的话,请使用3.8.1版本。我们在项目TestJUnit根目录下添加一个新目录testsrc,并把它加入到项目源代码目录中(加入方式见图3)以存放测试代码,使之与源代码分离。图3一切准备就绪,下面是编写的单元测试用例:packagecom.meritit;importstaticorg.junit.Assert.AssertNull;importstaticorg.junit.Assert.AssertEquals;importorg.junit.Test;publicclassTestWordDealUtil{//测试wordFormat4DB一般的处理情况@TestpublicvoidwordFormat4DBNormal(){Stringtarget=employeeInfo;Stringresult=WordDealUtil.wordFormat4DB(target);assertEquals(employee_info,result);}//测试null时的处理情况@TestpublicvoidwordFormat4DBNull(){Stringtarget=null;Stringresult=WordDealUtil.wordFormat4DB(target);assertNull(result);}//测试空字符串时的处理情况@TestpublicvoidwordFormat4DBEmpty(){Stringtarget=;Stringresult=WordDealUtil.wordFormat4DB(target);assertEquals(,result);}//测试当首字母大写时的情况@TestpublicvoidwordFormat4DBegin(){Stringtarget=EmployeeInfo;Stringresult=WordDealUtil.wordFormat4DB(target);assertEquals(employee_info,result);}//测试当尾字母为大写时的情况@TestpublicvoidwordFormat4DBEnd(){Stringtarget=employeeInfoA;Stringresult=WordDealUtil.wordFormat4DB(target);assertEquals(employee_info_a,result);}//测试多个相连字母大写时的情况@TestpublicvoidwordFormat4DBTogether(){Stringtarget=employeeAInfo;Stringresult=WordDealUtil.wordFormat4DB(target);assertEquals(employee_a_info,result);}}3.3编写代码按照需求所写的代码如下:packagecom.meritit;importjava.util.regex.Matcher;importjava.util.regex.Pattern;/***对名称、地址等字符串格式的内容进行格式检查*@authorlvxg*/publicclassWordDealUtil{/***将Java对象名称(每个单词的头字母大写)按照*数据库命名的习惯进行格式化*格式化后的数据为小写字母,并且使用下划线分割命名单词**例如:employeeInfo经过格式化之后变为employee_info**@paramnameJava对象名称*/publicstaticStringwordFormat4DB(Stringname){Patternp=Pattern.compile([A-Z]);Matcherm=p.matcher(name);StringBuffersb=newStringBuffer();while(m.find()){m.appendReplacement(sb,_+m.group());}returnm.appendTail(sb).toString().toLowerCase();}}运行测试,结果如图4所示:图4JUnit运行界面提示我们有两个测试情况未通过——当首字母大写时得到的处理结果与预期的有偏差,造成测试失败(failure);而当测试对null的处理结果时,则直接抛出了异常——测试错误(error)。JUnit将测试失败的情况分为两种:failure和error。Failure一般由单元测试使用的断言方法判断失败引起,它表示在测试点发现了问题;而error则是由代码异常引起,这是测试目的之外的发现,它可能产生于测试代码本身的错误(测试代码也是代码,同样无法保证完全没有缺陷),也可能是被测试代码中的一个隐藏的bug。显然,被测试代码中并没有对首字母大写和null这两种特殊情况进行处理,现在对源代码进行修改,添加对这两种情况的处理,修改后的代码如下:packagecom.meritit;importjava.util.regex.Matcher;importjava.util.regex.Pattern;/***对名称、地址等字符串格式的内容进行格式检查*@authorlvxg*/publicclassWordDealUtil{/***将Java对象名称(每个单词的头字母大写)按照*数据库命名的习惯进行格式化*格式化后的数据为小写字母,并且使用下划线分割命名单词**例如:employeeInfo经过格式化之后变为employee_info**@paramnameJava对象名称*/publicstaticStringwordFormat4DB(Stringname){if(name==null){returnnull;}Patternp=Pattern.compile([A-Z]);Matcherm=p.matcher(name);StringBuffersb=newStringBuffer();while(m.find()){if(m.start()!=0)m.appendReplacement(sb,(_+m.group()).toLowerCase());}returnm.appendTail(sb).toString().toLowerCase();}}再次运行测试,显示的测试结果如图5所示:图5至此,现在的代码已经比较稳定,可以作为API的一部分提供给其它模块使用了,如果测试还没有通过,则继续进行代码改进,一直等到测试完全通过时编码才完成。您的单元测试代码不是用来证明您是对的,而是为了证明您没有错。因此单元测试的范围要全面,比如对边界值、正常值、错误值的测试;对代码可能出现的问题要全面预测,而这也正是需求分析、详细设计环节中要考虑的。四.JUnit深入当然,JUnit提供的功能决不仅仅如此简单,在接下来的内容中,我们会看到JUnit中很多有用的特性,掌握它们对您灵活的编写单元测试代码非常有帮助。4.1Fixture何谓Fixture?它是指在执行一个或者多个测试方法时需要的一系列公共资源或者数据,例如测试环境,测试数据等等。在编写单元测试的过程中,您会发现在大部分的测试方法在进行真正的测试之前都需要做大量的铺垫——为设计准备Fixture而忙碌。这些铺垫过程占据的代码往往比真正测试的代码多得多,而且这个比率随着测试的复杂程度的增加而递增。当多个测试方法都需要做同样的铺垫时,重复代码的“坏味道”便在测试代码中弥漫开来。这股“坏味道”会弄脏您的代码,还会因为疏忽造成错误,应该使用一些手段来根除它。JUnit专门提供了设置公共Fixture的方法,同一测试类中的所有测试方法都可以共用它来初始化Fixture和注销Fixture。和编写JUnit测试方法一样,公共Fixture的设置也很简单,您只需要:1.使用注解org,junit.Before修饰用于初始化Fixture的方法。2.使用注解org.junit.After修饰用于注销Fixture的方法。3.保证这两种方法都使用publicvoid修饰,而且不能带有任何参数。遵循上面的三条原则,编写出的代码大体是这个样子://初始化Fixture方法@Beforepublicvoidinit(){……}//注销Fixture方法@Afterpublicvoiddestroy(){……}这样,在每一个测试方法执行之前,JUnit会保证init方法已经提前初始化测试环境,而当此测试方法执行完毕之后,JUnit又会调用destroy方法注销测试环境。注意是每一个测试方法的执行都会触发对公共Fixture的设置,也就是说使用注解Before或者After修饰的公共Fixture设置方法是方法级别的(图6)。这样便可以保证各个独立的测试之间互不干扰,以免其它测试代码修改测试环境或者测试数据影响到其它测试代码的准确性。图6可是,这种Fixture设置方式还是引来了批评,因为它效率低下,特别是在设置Fixture非常耗时的情况下(例如设置数据库链接)。而且对于不会发生变化的测试环境或者测试数据来说,是不会影响到测试方法的执行结果的,也就没有必要针对每一个测试方法重新设置一次Fixture。因此在JUnit4中引入了类级别的Fixture设置方法,编写规范如下:1.使用注解org,junit.BeforeClass修饰用于初始化Fixture的方法。2.使用注解org.junit.Aft