使用 EMMA 測量測試覆蓋率

來源:互聯網
上載者:User
EMMA 是一個用於檢測和報告 JAVA 程式碼涵蓋範圍的開源工具。它不但能很好的用於小型項目,很方便得得出覆蓋率報告,而且適用於大型企業層級的項目。

EMMA 有許多優點,首先你能免費得到它,並把它用於自己項目的開發。它支援許多種層級的覆蓋率指標:包,類,方法,語句塊(basic block)和行,特別是它能測出某一行是否只是被部分覆蓋,如條件陳述式短路的情況。它能產生 text,xml,html 等形式的報告,以滿足不同的需求,其 html 報告提供下鑽功能,我們能夠從 package 開始一步步連結到我們所關注的某個方法。EMMA 能和 Makefile 和 Ant 整合,便於應用於大型項目。特別還須指出的一點是,EMMA 的效率很高,這對於大型項目來說很重要。

EMMA 是通過向 .class 檔案中插入位元組碼的方式來追蹤記錄被運行代碼資訊的。EMMA 支援兩種模式:On the fly 和 Offline 模式。

On the fly 模式往載入的類中加入位元組碼,相當於用 EMMA 實現的 application class loader 替代原來的 application class loader。

Offline 模式在類被載入前,加入位元組碼。

On the fly 模式比較方便,缺點也比較明顯,如它不能為被 boot class loader 載入的類產生覆蓋率報告,也不能為像 J2EE 容器那種自己有獨特 class loader 的類產生覆蓋率報告。這時,我們能求助於 Offline 模式。

EMMA 也支援兩種運行方式:Command line 和 Ant。

命令列一般和 On the fly 模式一起適用,對於簡單的項目能夠快速產生覆蓋率報告。通過 Ant task 來運行 EMMA 的話,特別適用於大型的項目。

本文後面提供的執行個體主要是示範如何整合 EMMA 和 Ant,通過 Offline 模式產生覆蓋率報告。





回頁首

樣本項目

樣本工程 SampleProject 是個小型的項目,有一個類 NumberParser,主要功能是把一個字串解析成 float 型。下面是整個工程的目錄結構。


圖1. 樣本項目的目錄結構

下面,我們開始來為我們的工程編寫 Ant 指令碼。


清單1設定一些屬性,包括源檔案,二進位檔案,JUnit 報告,覆蓋率報告等的路徑


<!-設定Java類被注入位元組碼後存放的路徑-->
<property name="bin.instrument.dir" location="../instrbin" />
<!-設定覆蓋率中繼資料和報告的路徑-->
<property name="coverage.dir" location="../coverage" />
<!--設定junit報告的路徑 -->
<property name="junitReport.dir" location="../junitReport" />
<!-設定主題代碼bin路徑-->
<property name="bin.main.dir" location="../srcbin" />
<!-設定測試代碼bin路徑-->
<property name="bin.test.dir" location="../testbin" />
<!--設定主題代碼源路徑-->
<property name="src.main.dir" location="../../SampleProject/src" />
<!--設定測試代碼源路徑-->
<property name="src.test.dir" location="../../SampleProjectTest/test"
/>
<!-指示需要注入位元組碼的Java類的路徑-->
<path id="classpath.main">
<pathelement location="${bin.main.dir}" />
</path>
<!-指示 emma.jar 和emma_ant.jar 的路徑-->
<path id="emma.lib">
<pathelement location="${libs}/emma.jar" />
<pathelement location="${libs}/emma_ant.jar" />
</path>
<!-允許emma-->
<property name="emma.enabled" value="true" />


其中目錄${ bin.instrument.dir }存放被注入位元組碼的類,"emma.lib" 指向 emma 資源所在的位置。


清單2為 ANT 定義 EMMA 任務

<!-為ANT添加EMMA任務-->
<taskdef resource="emma_ant.properties" classpathref="emma.lib" />

清單3編譯原始碼和測試代碼

<target name="compile-src.main">
<mkdir dir="${bin.main.dir}" />
<javac destdir="${bin.main.dir}" debug="on">
<src path="${src.main.dir}" />
</javac>
<copy todir="${bin.main.dir}">
<fileset dir="${src.main.dir}">
<exclude name="**/*.java" />
</fileset>
</copy>
</target>

<target name="compile-src.test">
<mkdir dir="${bin.test.dir}" />
<javac destdir="${bin.test.dir}" debug="on">
<src path="${src.test.dir}" />
<classpath location="${bin.main.dir}" />
</javac>
<copy todir="${bin.test.dir}">
<fileset dir="${src.test.dir}">
<exclude name="**/*.java" />
</fileset>
</copy>
</target>


編譯分兩階段,先編譯原始碼,然後再編譯測試案例代碼。


清單4在所要測試類別的代碼中插入位元組碼

<!-對編譯在路徑bin.main.dir中的Java類注入位元組碼, 
並且把注入位元組碼的新Java類存放到路徑bin.instrument.dir-->
<!-覆蓋率的中繼資料存放在路徑coverage.dir中-->
<target name="instrument">
<mkdir dir="${bin.instrument.dir}" />
<mkdir dir="${coverage.dir}" />
<emma enabled="${emma.enabled}">
<instr instrpathref="classpath.main"
destdir="${bin.instrument.dir}"
metadatafile="${coverage.dir}/metadata.emma"
merge="true">
</instr>
</emma>
<copy todir="${bin.instrument.dir}">
<fileset dir="${bin.main.dir}">
<exclude name="**/*.java" />
</fileset>
</copy>
</target>


當${emma.enabled}為 true 時,才產生插入位元組碼的類。<instr>中指定了要 instrument 的類的地址, instrumented 後類存放的地址,以及 metadata 存放的地址。


清單5運行測試案例,得到一些產生報告的中繼資料


<!-執行測試案例同時產生junit測試報告和emma程式碼涵蓋範圍報告-->
<target name="test">
<mkdir dir="${junitReport.dir}" />
<junit fork="true" forkmode="once"
printsummary="withOutAndErr"
errorproperty="test.error"
showoutput="on">
<!-指明程式碼涵蓋範圍的中繼資料的存放位置-->
<jvmarg
value="-Demma.coverage.out.file=${coverage.dir}/metadata.emma" />
<jvmarg value="-Demma.coverage.out.merge=true" />
<classpath location="${bin.instrument.dir}" />
<classpath location="${bin.test.dir}" />
<classpath refid="emma.lib" />

<formatter type="xml" />
<!-執行所有以Test結尾的junit測試案例-->
<batchtest todir="${junitReport.dir}" haltonfailure="no">
<fileset dir="${bin.test.dir}">
<include name="**/*Test.class" />
</fileset>
</batchtest>
</junit>

</target>


在運行測試案例前,需要設定 jvmarg。所有的測試案例都跑在 instrumented 的類上面。


清單6產生 JUnit 報告


<target name="gen-report-junit">
<!-產生junit測試報告-->
<junitreport todir="${junitReport.dir}">
<fileset dir="${junitReport.dir}">
<include name="*" />
</fileset>
<report format="frames" todir="${junitReport.dir}" />
</junitreport>
</target>

清單7產生覆蓋率報告

<!-產生程式碼涵蓋範圍報告-->
<target name="gen-report-coverage">
<!-如果屬性emma.enabled的值是true,就產生程式碼涵蓋範圍報告 -->
<emma enabled="${emma.enabled}">
<report sourcepath="${src.main.dir}"
sort="+block,+name,+method,+class"
metrics="method:70,block:80,line:80,class:100">
<fileset dir="${coverage.dir}">
<include name="*.emma" />
</fileset>
<html outfile="${coverage.dir}/coverage.html"
depth="method" columns="name,class,method,block,line" />
</report>
</emma>
</target>


<report>中 sourcepath 指明原始碼所在的位置,以便能夠顯示每行代碼的覆蓋情況。Sort指明產生列表的排列順序,"+"表示升序,"-"表示降序。Metrics 可為每個度量指明一個覆蓋率閾值,若未達到該閾值,則該行會被標記出來(前提是報告的形式支援這個功能,如 HTML)。<html>指明以 HTML 形式產生報告,Depth 指明報告的詳細程度,columns 指明產生列表列名的排列順序。





回頁首

顯示報告

我們已經寫好了Ant指令碼,接下來你就可以運行該指令碼了。這裡假設你已經搭好了運行 Ant 和 JUnit 的環境,直接到指令碼所在目錄,在命令列敲入 Ant 即可。

下面是各個層次的報告:


圖2整個項目層次的報告

圖3包層次的報告

圖4類層次的報告

圖5用顏色標記的原始碼

你會發現有三種顏色,綠色,紅色和黃色,它們分別表示該行:被測試到,未被測試到,以及部分被測試到。紅色或黃色的部分是需要引起你注意的,bug 也許就隱藏在這部分代碼中,你所需做的就是設計一些測試案例,使它們運行以前未被執行到的語句。如上面那張圖給出了我們一些資訊,String 中含有"+"號的情況未被測試到,還有"isPositive"只被測試到 true 或 false 的一種情況,你需要相應的增加一些測試案例。運行新加的測試案例,你也許會發現一些新的 bug,並修正這些 bug。





回頁首

隱藏在報告背後的問題

對於這個簡單的例子,你會發現,我們很容易達到 100% 的測試覆蓋率,你也許會鬆口氣說:啊,我把所有情況都測試到了,這下放心了。在這裡很遺憾的告訴你,EMMA 的功能是有限的,它不支援決策覆蓋和路徑覆蓋。事實上,對於一個稍複雜的工程進行窮盡的測試是不可能的。


清單8決策覆蓋和路徑覆蓋的程式碼範例


/**
* Parses the given string to a float number
*
* @param number
* the given string
* @return the float number related with the string
*
* @throws IllegalArgumentException
* if the string is empty, null or can not parse to a float
*/
public float parse(String number) {
if (number.equals("")||number == null ) {
throw new IllegalArgumentException(
"Number string should not be empty or null");
}
StringIterator stringIterator = new StringIterator(number);
getSign(stringIterator);
int integer = getInteger(stringIterator);
float fraction = getFraction(stringIterator);
float total = integer + fraction;
return isPositive ? total : (-1) * total;
}

清單9決策覆蓋和路徑覆蓋的測試案例

    
public void test_parse () {
NumberParser np = new NumberParser();
String number ="";
try {
np.parse(number);
fail("should throw IAE");
} catch (IllegalArgumentException e) {
// pass
}
number = "22.010";
float parsedNumber = np.parse(number);
assertEquals((float) 22.010, parsedNumber);

number = "-22.010";
parsedNumber = np.parse(number);
assertEquals((float) 22.010, parsedNumber);

}


運行 Ant 指令碼,產生報告,你會發現,測試案例都運行通過了,測試覆蓋報告也表明代碼所有的行都被執行到了。但細心的讀者肯定早已看到上面代碼存在 Bug。若傳進 parse 的 string 為 null 的話,並不是如我們所願,得到 IllegalArgumentException,而是拋出了 NullPointerException。

雖然下面那行是綠色的,但它只表明每個條件陳述式都被執行到了,並不能說明每個條件都取到true和false兩種情況。在我們設計的測試案例中, "null == number"只取到 false 一種情況。我們需要在我們的測試案例中加入對 string 情況是 null 的測試。


圖6 決策覆蓋和路徑覆蓋率報告

清單10 修正代碼的 Bug

                if (null == number || "".equals(number)) {




回頁首

結束語

為你的項目產生覆蓋率報告,EMMA 是個不錯的選擇。通過覆蓋率報告,我們能發現並修複一些隱藏的 bug,我們的軟體會變得更強壯

來源於IBM網站: http://www-128.ibm.com/developerworks/cn/java/j-lo-emma/index.html

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.