(转)运用Cobertura统计单元测试覆盖率

(转)使用Cobertura统计单元测试覆盖率
转载自: http://terrencexu.iteye.com/blog/718834
--- James Gosling mused: "I don't think anybody tests enough of anything."

做单元测试是developer都要接触的事情,工具也基本上都是选择JUnit或者TestNG,但是无论是JUnit还是TestNG都只能得出一个测试用例相关的报表。

从这个报表中我们能得信息是,测试用例的执行情况,成功率,失败率,哪个失败了等等。通过这份报表我们并不能得悉我们是否把所有的功能代码都测试到了,那么这时候我们就需要引入单元测试覆盖率的概念了。

单元测试覆盖率通俗的讲就是多少行代码被测试用例运行到了,多少个block被执行了,多少个包被执行了等,通过这些数据我们可以清楚的了解测试的覆盖率情况,进而反向的改善已有的或者新添加测试用例去尽可能多的覆盖功能代码,block等,以提高代码的可信赖度。

对于Java而言,进行覆盖率分析的方式有三类:第一种是将instrumentation(不知道怎么翻译好,测试仪表?),直接加入到源代码中;第二种是将instrumentation加入到编译好的Java字节码中;第三种是在一个可编辑的虚拟机中运行代码。Cobertura选择了第二种方式。

为了便于使用,Cobertura提供了两种方式将Cobertura集成到已有的运行环境中: Ant和命令行

总结起来Cobertura做的事情就是:

1. Cobertura将instrumentation加入到编译好的需要被检测的Java类的字节码中。
2. 运行测试用例的时候Cobertura通过之前安插好的instrumentation统计每一行代码是否被执行,所有被植入instrumentation的类的序列化信息将被写入cobertura.ser。
3. 根据统计结果生成报表,格式为XML或者HTML。

整个过程不需要我们额外写任何Java代码,只需要通过ant脚本或者命令行触发相应的操作。

下面首先介绍一下使用ant脚本的方式。

第一步,下载最新的Cobertura,解压。下载地址为http://cobertura.sourceforge.net/download.html。
第二步,将Cobertura目录下面的Cobertura.jar和lib下所有jar拷贝到你的工程的某个目录下。
第三步,创建ant脚本,或者在已有的ant脚本中添加相应的target。

现在开始设置ant脚本
第一步,将cobertura.jar以及Cobertura/lib下的所有jar引入classpath
  
<path id="lib.classpath">  
       <fileset dir="${lib.dir}">  
           <include name="**/*.jar"/>  
       </fileset>  
   </path>

注:lib.dir是你存放cobertura.jar以及/Conbertura/lib/*.jar的地方

第二步,将cobertura自身定义的task引入到ant脚本中
  
1. <taskdef classpathref="lib.classpath" resource="tasks.properties" />

第三步,编译工程代码到某个目录,比如${src.java.classes.dir}

注:你可以选择将所有的业务代码和测试代码编译到一个classes目录下,或者选择编译到不同的目录下,在本例中将使用不同的目录存放java.src和test.src。
  
<target name="compile" depends="init">  
       <javac srcdir="${src.java.dir}" destdir="${src.java.classes.dir}" debug="yes">  
           <classpath refid="lib.classpath" />  
       </javac>  
       <javac srcdir="${src.test.dir}" destdir="${src.test.classes.dir}" debug="yes">  
           <!-- This is very import to include the src.java.classes.dir here -->  
           <classpath location="${src.java.classes.dir}" />  
           <classpath refid="lib.classpath" />  
       </javac>  
   </target>

注:src.java.dir存放所有的将被测试的java类,src.java.classes.dir存放java类的编译字节码;src.test.dir存放所有的测试用例, src.test.classes.dir存放测试用例的编译字节码。init target用来创建一些备用的目录,将包含在附件的完整工程代码中。

第四步,定义target,向生成的java.class里插入instrumentation,test.class将不插入instrumentation,因为我们不关心测试用例本身的覆盖率。
  
<target name="instrument">  
       <!-- Remove the coverage data file and any old instrumentation classes -->  
       <delete file="cobertura.ser" />  
       <delete dir="${instrumented.classes.dir}" />  
     
       <!-- Instrument the application classes, writing the instrumented classes into ${instrumented.classes.dir} -->  
       <cobertura-instrument todir="${instrumented.classes.dir}">  
           <!-- The following line causes instrument to ignore any source line containing a reference to log4j, for the purpose of coverage reporting -->  
           <ignore regex="org.apache.log4j.*"/>  
           <fileset dir="${src.java.classes.dir}">  
               <include name="**/*.class"/>  
           </fileset>  
       </cobertura-instrument>  
   </target>

注:instrumented.classes.dir存在所有被植入instrumentation的Java class。如果java代码和测试用例被编译到了同一个目录下,可以使用如<exclude name="**/*Test.class" />忽略测试用例。

第五步,执行测试用例,同时Cobertura将在后台统计代码的执行情况。这一步就是普通的junit的target,将执行所有的测试用例,并生成测试用例报表。
<target name="test" depends="init,compile">
    <junit fork="yes" dir="${basedir}" failureProperty="test.failed">
        <!-- Note: the classpath order: instrumented classes are before the original (uninstrumented) classes. This is important!!! -->
        <classpath location="${instrumented.classes.dir}" />
        <classpath location="${src.java.classes.dir}" />
        <classpath location="${src.test.classes.dir}" />
        <classpath refid="lib.classpath" />
        
		<formatter type="xml"/>
        <test name="${testcase}" todir="${reports.junit.xml.dir}" if="testcase" />
        <batchtest todir="${reports.junit.xml.dir}" unless="testcase">
            <fileset dir="${src.test.dir}">
                <include name="**/*.java"/>
            </fileset>
        </batchtest>
    </junit>

    <junitreport todir="${reports.junit.xml.dir}">
        <fileset dir="${reports.junit.xml.dir}">
            <include name="TEST-*.xml"/>
        </fileset>
        <report format="frames" todir="${reports.junit.html.dir}"/>
    </junitreport>
</target>


注:这一步非常需要注意的是${instrumented.classes.dir}应该最先被引入classpath.
It is important to set fork="true" because of the way Cobertura works. It only flushes its changes to the coverage data file to disk when the JVM exits. If JUnit runs in the same JVM as ant, then the coverage data file will be updated AFTER ant exits, but you want to run cobertura-report BEFORE ant exits. 

For this same reason, if you're using ant 1.6.2 or higher then you might want to set forkmode="once". This will cause only one JVM to be started for all your JUnit tests, and will reduce the overhead of Cobertura reading/writing the coverage data file each time a JVM starts/stops.

第六步,生成测试覆盖率报表。
<target name="alternate-coverage-report">
    <!-- Generate a series of HTML files containing the coverage data in a user-readable form using nested source filesets -->
    <cobertura-report destdir="${coverage.cobertura.html.dir}">
        <fileset dir="${src.java.dir}">
            <include name="**/*.java"/>
        </fileset>
    </cobertura-report>
</target>

注:因为我将Java代码和测试用例分别放在不同的包中,所以如果你的代码都放在一个包中的话,应该使用<exclude name="**/*Test.java" />剔除测试用例; coverage.cobertura.html.dir是存放report的地方。生成XML报表的方式将在完成的 build.xml文件中给出。

到此我们已经完成了生成测试覆盖率报表的全部工作,如果还想验证一下测试覆盖率,可以通过以下方式
<target name="coverage-check">
    <cobertura-check branchrate="34" totallinerate="100" />
</target>

If you do not specify branchrate, linerate, totalbranchrate or totallinerate, then Cobertura will use 50% for all of these values.
引用自: http://cobertura.sourceforge.net/anttaskreference.html
<cobertura-check branchrate="30" totalbranchrate="60" totallinerate="80">
    <regex pattern="com.example.reallyimportant.*" branchrate="80" linerate="90"/>
    <regex pattern="com.example.boringcode.*" branchrate="40" linerate="30"/>
</cobertura-check>

cobertura-merge Task
You must tell Cobertura which coverage data files to merge by passing in standard ant filesets.

<cobertura-merge>
    <fileset dir="${test.execution.dir}">
        <include name="server/cobertura.ser" />
        <include name="client/cobertura.ser" />
    </fileset>
</cobertura-merge>

现在给出完成的build.xml文件,仅供参考:
<?xml version="1.0" encoding="UTF-8"?>
<project name="study-cobertura" default="coverage" basedir=".">
<description>The ant file for study-cobertuna</description>

<property name="src.java.dir" value="${basedir}/java" />
<property name="src.test.dir" value="${basedir}/test" />
<property name="build.dir" value="${basedir}/build" />
<property name="src.java.classes.dir" value="${build.dir}/src-java-classes" />
<property name="src.test.classes.dir" value="${build.dir}/src-test-classes" />
<property name="instrumented.classes.dir" value="${build.dir}/instrumented-classes" />
<property name="reports.dir" value="${basedir}/reports" />
<property name="reports.junit.xml.dir" value="${reports.dir}/junit-xml" />
<property name="reports.junit.html.dir" value="${reports.dir}/junit-html" />
<property name="coverage.cobertura.xml.dir" location="${reports.dir}/cobertura-xml"/>
<property name="coverage.cobertura.summary.dir" location="${reports.dir}/cobertura-summary-xml"/>
<property name="coverage.cobertura.html.dir" location="${reports.dir}/cobertura-html"/>
<property name="lib.dir" location="${basedir}/lib"/>

<path id="lib.classpath">
	<fileset dir="${lib.dir}">
		<include name="**/*.jar"/>
	</fileset>
</path>
 
<taskdef classpathref="lib.classpath" resource="tasks.properties" />
 
<target name="init">
	<mkdir dir="${src.java.classes.dir}"/>
	<mkdir dir="${src.test.classes.dir}"/>
	<mkdir dir="${instrumented.classes.dir}"/>
	<mkdir dir="${reports.dir}"/>
	<mkdir dir="${reports.junit.html.dir}"/>
	<mkdir dir="${reports.junit.xml.dir}"/>
	<mkdir dir="${coverage.cobertura.xml.dir}"/>
	<mkdir dir="${coverage.cobertura.summary.dir}"/>
	<mkdir dir="${coverage.cobertura.html.dir}"/>
</target>
 
<target name="compile" depends="init">
	<javac srcdir="${src.java.dir}" destdir="${src.java.classes.dir}" debug="yes">
	 <classpath refid="lib.classpath" />
	</javac>
	<javac srcdir="${src.test.dir}" destdir="${src.test.classes.dir}" debug="yes">
	 <!-- This is very import to include the src.java.classes.dir here -->
	 <classpath location="${src.java.classes.dir}" />
	 <classpath refid="lib.classpath" />
	</javac>
</target>
 
<target name="instrument">
	<!-- Remove the coverage data file and any old instrumentation classes -->
	<delete file="cobertura.ser" />
	<delete dir="${instrumented.classes.dir}" />

	<!-- Instrument the application classes, writing the instrumented classes into ${instrumented.classes.dir} -->
	<cobertura-instrument todir="${instrumented.classes.dir}">
	 <!-- The following line causes instrument to ignore any source line containing a reference to log4j, for the purpose of coverage reporting -->
	 <ignore regex="org.apache.log4j.*"/>
	 <fileset dir="${src.java.classes.dir}">
		<include name="**/*.class"/>
	 </fileset>
	</cobertura-instrument>
</target>
 
<target name="test" depends="init,compile">
	<junit fork="yes" dir="${basedir}" failureProperty="test.failed">
		 <!-- Note: the classpath order: instrumented classes are before the original (uninstrumented) classes. This is important!!! -->
		 <classpath location="${instrumented.classes.dir}" />
		 <classpath location="${src.java.classes.dir}" />
		 <classpath location="${src.test.classes.dir}" />
		 <classpath refid="lib.classpath" />

		 <formatter type="xml"/>
		 <test name="${testcase}" todir="${reports.junit.xml.dir}" if="testcase" />
		 <batchtest todir="${reports.junit.xml.dir}" unless="testcase">
			 <fileset dir="${src.test.dir}">
				<include name="**/*.java"/>
			</fileset>
		 </batchtest>
	</junit>

	<junitreport todir="${reports.junit.xml.dir}">
		 <fileset dir="${reports.junit.xml.dir}">
			<include name="TEST-*.xml"/>
		 </fileset>
		 <report format="frames" todir="${reports.junit.html.dir}"/>
	</junitreport>
</target>
 
<target name="coverage-check">
	<cobertura-check branchrate="34" totallinerate="100" />
</target>
 
<!-- =================================
target: coverage-report 
================================= -->
<target name="coverage-report">
	<!-- Generate an XML file containing the coverage data using the 'srcdir' attribute -->
	<cobertura-report srcdir="${src.java.dir}" destdir="${coverage.cobertura.xml.dir}" format="xml" />
</target>

<!-- =================================
target: summary-coverage-report 
================================= -->
<target name="summary-coverage-report">
	<!-- Generate an summary XML file containing the coverage data using the 'srcidir' attribute -->
	<cobertura-report srcdir="${src.java.dir}" destdir="${coverage.cobertura.summary.dir}" format="summaryXml" />
</target>
 
<!-- =================================
target: alternate-coverage-report 
================================= -->
<target name="alternate-coverage-report">
	<!-- Generate a series of HTML files containing the coverage data in a user-readable form using nested source filesets -->
	<cobertura-report destdir="${coverage.cobertura.html.dir}">
		<fileset dir="${src.java.dir}">
			<include name="**/*.java"/>
		</fileset>
	</cobertura-report>
</target>

<!-- =================================
target: clean 
================================= -->
<target name="clean" description="Remove all files created by the build/test process">
	<delete dir="${src.java.classes.dir}" />
	<delete dir="${src.test.classes.dir}" />
	<delete dir="${instrumented.classes.dir}" />
	<delete dir="${reports.dir}" />
	<delete file="cobertura.log" />
	<delete file="cobertura.ser" />
</target>

<!-- =================================
target: coverage 
================================= -->
<target name="coverage" depends="clean, compile, instrument, test, coverage-report, summary-coverage-report, alternate-coverage-report" description="Compile, instrument ourself, run the tests and generate JUnit and coverage reports." />

</project>


一定要在ant清除脚本中加入删除cobertura.ser的ant脚本。因为当第一次运行cobertura时,会产生cobertura.ser,以后想屏蔽掉新增的测试类会不起作用,因为cobertura.ser老文件没有被删除会一直被沿用。
<target name="cleanup">
		<delete dir="${builddir}" />
		<delete dir="${distdir}" />
		<delete dir="${instrumenteddir}"/>
		<!-- clean up the classes coverage log instrumention info-->
		<delete file="cobertura.ser"/>
</target>