单元测试实践
1.测试框架选择Unitils
为什么选择Untils作为本次项目单元测试框架呢?
Unitils的优点和介绍网上都比较详细:http://www.unitils.org/summary.html;它的主要模块有:
· DatabaseModule: 测试数据库维护和连接池
· DbUnitModule: 使用DBunit进行测试数据维护
· HibernateModule: Hibernate配置支持和自动的数据库mapping检查
· MockModule: 支持使用Unitils的mock框架进行mock创建
· EasyMockModule: 支持使用EasyMock的mock框架进行mock创建
· InjectModule: 支持注入mock对象到其他对象中
· SpringModule: 支持载入Spring配置文件、检索或注入Spring
Beans
本次项目并没有全部用到它的所有特性,其中我使用到的优秀特性和功能有:
· 为Spring集成的单元测试提供很好的解决方案
· 数据库相关测试的数据准备,事务回滚
· 简单方便的Assert工具
2.在测试中使用Unitils
方法一. 继承UnitilsJUnit4
方法二. 在测试类上添加annotation:@RunWith(UnitilsJUnit4TestClassRunner.class)
方法三. 拷贝UnitilsJUnit3(UnitilsJUnit4)的代码生成一个MyUnitilsJunit3,然后测试类继承这个类。这种方法的好处是MyUnitilsJunit3可以随意继承一个Junit3
TestCase 的子类,比如: AbstractDependencyInjectionSpringContextTests,从而可以更加方便的加入自己需要的功能。
3.使用Unitils进行Spring集成的单元测试
在基类中设置公用的Spring 配置
@SpringApplicationContext( {"/bean/profile/base-beans.xml"})
public class DAOTestBase extends UnitilsJUnit4{
}
子类中特殊化的Spring配置
public class UserDAOTest extends DAOTestBase{
@SpringApplicationContext( {"/bean/profile/base-beans.xml","/bean/profile/extra-beans.xml"})
protected ApplicationContext applicationContext;
@SpringBean("userDAO")
private UserDAO userDAO;
@SpringBeanByName
private UserDAO userDAO;
@SpringBeanByType
private UserDAO userDAO;
}
4.使用DBunit进行数据库相关的测试
(1). 在unitils.properties当中进行配置
database.driverClassName=com.mysql.jdbc.Driver
database.url=jdbc:mysql://192.168.205.62:3310/pro_general?characterEncoding=UTF-8
database.userName=profile
database.password=profile
database.schemaNames=pro_general
database.dialect=mysql
设置数据载入策略为先删除再插入。常用的载入策略有CleanInsertLoadStrategy;InsertLoadStrategy;RefreshLoadStrategy;UpdateLoadStrategy;顾名思义,这些策略不难理解。
DbUnitModule.DataSet.loadStrategy.default=org.unitils.dbunit.datasetloadstrategy.impl.DeleteInsertLoadStrategy
数据集的格式支持多种,常用的有xml和excel,本项目中使用excel的xls文件(不是xlsx)。毕竟excel的编辑更加方便。下面的配置指定默认数据集解析方式:
DbUnitModule.DataSet.factory.default=org.unitils.dbunit.datasetfactory.impl.XlsDataSetFactory
(2). 在测试类中指定数据加载
@DataSet
public class TagTest extends DAOTestBase
{
@ExpectedDataSet
public void testUpdate(){
}
}
Annotation @DataSet指明该类下的所有测试方法执行前都需要进行数据准备。DataSet中可以指明数据文件的具体路径和文件名,如果没有指定,默认在执行测试方法前先找${ClassName.methodName}的数据文件,再找${ClassName}的数据文件(文件格式为前面设置过的数据集格式)。
@ExpectedDataSet用于检查执行结果是否和预期一致。预期数据集文件和前面的数据准备文件查找过程类似只是文件名后面多了个“-result”。
5.使用Unitils事务保障数据库相关测试的一致性
@Transactional(TransactionMode.ROLLBACK)
在测试类或方法上添加Transactional Anotation,用于指定事务执行方式。这里提一下Unitils的一个bug,使用Transactional
Anotation时测试类必须继承UnitilsJUnit4,用@RunWith的方式则不行。
测试覆盖率
之前的项目采用Clover来进行测试覆盖率的测算,用着还不错,挺好用的。但是由于Clover是非开源的,要给钱,免费的licence过期了,只能换一个开源的了。这次选用了cobertura,一个Jcoverage的分支,也有maven的插件。
在maven主pom进行配置:
<build>
<plugins>
……
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.4-SNAPSHOT</version>
<configuration>
<formats>
<format>html</format>
<format>xml</format>
</formats>
<instrumentation>
<!--<ignore>.*</ignore>
-->
<excludes>
<exclude>**/*Test.class</exclude>
<exclude>**/Abstract*TestCase.class</exclude>
<exclude>**/*Constants.class</exclude>
<exclude>**/*interface/*.class</exclude>
<exclude>**/*domain/*.class</exclude>
<exclude>**/*dataobject/*.class</exclude>
<exclude>**/web/**/*.class</exclude>
</excludes>
</instrumentation>
</configuration>
</plugin>
......
</plugins>
</build>
执行mvn cobertura:cobertura命令即可得出测试覆盖率报表,其中主要包括Line
coverage,branch coverage。得出的报表是以子项目为单位的。由于cobertura在maven插件中并未提供merge的功能,所有只依靠cobertura-maven-plugin无法得出整个项目的测试覆盖率。
但是可以依靠其他手段获得整个项目的。具体实施方法如下:
1. 下载coberturahttp://cobertura.sourceforge.net/download.html.(如果不想自己打包,可以直接下载bin文件而不用src.如果对其源码敢兴趣可以下载src,cobertura是基于ant构建的)
2. 执行 mvn cobertura:cobertura 命令。执行完成后在各个子项目的target/cobertura里面会生成cobertura.ser文件。但是主pom对应的target/cobertura目录下并没有cobertura.ser
3.将子项目的cobertura.ser进行merge,生成整个项目的cobertura.ser文件。
../cobertura-1.9.3/cobertura-merge.sh
--datafile ./target/cobertura/cobertura.ser ./profile-ao/target/cobertura/cobertura.ser
./profile-core/target/cobertura/cobertura.ser ./profile-dal/target/cobertura/cobertura.ser
./profile-client/target/cobertura/cobertura.ser ./profile-common/target/cobertura/cobertura.ser
执行完上面命令后,在主pom对应的target/cobertura目录下会生成一个整个项目的cobertura.ser文件
4. 再执行mvn cobertura:cobertura命令。执行完成后,整个项目的测试覆盖率就生成了。
持续集成+测试覆盖率
测试覆盖率作为持续集成不可或缺的一个质量指标,将测试覆盖率和持续集成进行整合当然也就必不可少了。本项目持续集成平台采用CruiseControl
1.测试覆盖率结果作为持续集成是否通过的指标
(1)在maven中对cobertura的 check属性进行配置
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<configuration>
<check>
<branchRate>85</branchRate>
<lineRate>85</lineRate>
<haltOnFailure>true</haltOnFailure>
<totalBranchRate>85</totalBranchRate>
<totalLineRate>85</totalLineRate>
<packageLineRate>85</packageLineRate>
<packageBranchRate>85</packageBranchRate>
<regexes>
<regex>
<pattern>com.taobao.memberprofile.core.*</pattern>
<branchRate>90</branchRate>
<lineRate>80</lineRate>
</regex>
<regex>
<pattern> com.taobao.memberprofile.dal.*</pattern>
<branchRate>40</branchRate>
<lineRate>30</lineRate>
</regex>
</regexes>
</check>
</configuration>
<executions>
<execution>
<goals>
<goal>check</goal>
</goals>
</execution>
</executions>
</plugin>
(2)在持续集成的时候加入cobertura的check过程
修改CruiseControl的配置config.xml
<schedule Interval="60">
<maven2 MvnHome="/opt/taobao/install/apache-maven-2.1.0"
PomFile="${checkoutdir}/${project.name}/pom.xml"
Goal="install cobertura:check
cobertura:cobertura" />
</schedule>
2.将测试覆盖率结果展现集成到持续集成结果展现平台中
(1)将测试覆盖率报表放到artifacts目录里面。因为原来目录里面是没办法直接访问到的。
修改CruiseControl的配置config.xml
<artifactspublisher dir="${checkoutdir}/${project.name}/target/site/cobertura"
subdirectory="cobertura"
dest="artifacts/${project.name}/">
(2)集成到CruiseControl老的展示页面中(cruisecontrol路径下的)
首先,修改main.jsp,添加cobertura的tab:
<cruisecontrol:tabsheet>
<cruisecontrol:tab name="buildResults"
label="Build Results" >
<%@ include file="buildresults.jsp"
%>
</cruisecontrol:tab>
<cruisecontrol:tab name="TestCoverage"
label="Test Coverage" >
<%@ include file="cobertura.jsp"
%>
</cruisecontrol:tab>
<cruisecontrol:tab name="testResults"
label="Test Results" >
<%@ include file="testdetails.jsp"
%>
</cruisecontrol:tab>
<cruisecontrol:tab name="metrics"
label="Metrics" >
<%@ include file="metrics.jsp"
%>
</cruisecontrol:tab>
</cruisecontrol:tabsheet>
然后,添加cobertura.jsp,其内容为:
<%@ taglib uri="/WEB-INF/cruisecontrol-jsp11.tld"
prefix="cruisecontrol"%>
<cruisecontrol:xsl xslFile="/xsl/header.xsl"/>
<p>
<cruisecontrol:artifactsLink>
<iframe name="CoberturaFrame"
id="cloverFrame" style="width:100%; height:600;"
marginheight="10" frameborder="0"
marginwidth="10"
src="<%= artifacts_url %>/cobertura/index.html">
</iframe>
</cruisecontrol:artifactsLink>
</p>
其效果为:
(3)集成到新的展示页面中(dashboard路径下的)
首先修改webapps/dashboard/WEB-INF/vm/build_detail/build_detail_pass
ed.vm:
#parse("build_detail/build_detail_partial_header.vm")
<div class="build_detail_container">
<div class="sub_tab_container_menu">
<ul>
<li class="current_tab tab_toggle"><a><span>Artifacts</span></a></li>
<li class="tab_toggle"><a><span>Modifications</span></a></li>
<li class="tab_toggle"><a><span>Build
Log</span></a></li>
<li class="tab_toggle"><a><span>Tests</span></a></li>
<li class="tab_toggle"><a><span>Test
Coverage</span></a></li>
#parse('build_detail/build_detail_partial_widgets_tab.vm')
</ul>
</div>
<div class="sub_tab_container_content">
#set($artifacts_extra_attrs="")
#parse("build_detail/build_detail_partial_artifacts.vm")
#set($modification_extra_attrs="style='display:none'")
#parse("build_detail/build_detail_partial_modification.vm")
#set($log_extra_attrs="style='display:none'")
#parse("build_detail/build_detail_partial_log.vm")
#set($tests_extra_attrs="style='display:none'")
#parse("build_detail/build_detail_partial_tests.vm")
#set($coverage_extra_attrs="style='display:none'")
#parse("build_detail/build_detail_partial_testcoverage.vm")
#parse('build_detail/build_detail_partial_widgets_content.vm')
</div>
</div>
然后,在相同目录下新建build_detail_partial_testcoverage.vm,内容为:
<div id="test_coverage"
class="widget" $coverage_extra_attrs>
#if( $buildCmd.build.artifactFiles.size()
== 0)
<p>No Test Coverage Report found.</p>
#end
#foreach ($artifactFile in $buildCmd.build.artifactFiles)
#if($artifactFile.name.equals("cobertura"))
<iframe src="$url/index.html"
style="width:100%;" height=600
marginheight="10" frameborder="0"
marginwidth="10">
</iframe>
#end
#end
</div>
其效果为:
|