Emma使用与分析
什么是Emma
EMMA 是一個開源、面向 Java 程序測試覆蓋率收集和報告工具。
它通過對編譯后的 Java 字節(jié)碼文件進行插裝,在測試執(zhí)行過程中收集覆蓋率信息,并通過支持多種報表格式對覆蓋率結(jié)果進行展示。 EMMA 所使用的字節(jié)碼插裝不僅保證 EMMA 不會給源代碼帶來“臟代碼”,還確保 EMMA 擺脫了源代碼的束縛,這一特點使 EMMA 應(yīng)用于功能測試成為了可能。
如何使用
emma現(xiàn)在可以通過命令行,ant,maven,Jenkins等方式使用,這里只介紹通過maven和Jenkins來集成emma測試。
在Maven中的使用
直接運行mvn emma:emma,即可。
maven集成emma,需要兩個插件,maven-surefire-plugin和emma-maven-plugin,如果之前沒有安裝,那么maven會自動下載這兩個插件。
emma依賴于surefire的配置,默認執(zhí)行src/test/java的junit測試。為了方便使用,最好在自己的pom里配置maven-surefire-plugin插件。
<plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-surefire-plugin</artifactId><version>2.8.1</version><configuration><skipTests>false</skipTests><junitArtifactName>junit:junit</junitArtifactName><includes><include>**/*Test.java</include></includes><excludes><exclude>**/*_Roo_*</exclude></excludes></configuration> </plugin>這樣指定 maven-surefire-plugin 的版本為2.8.1,<skipTests>false</skipTests>不跳過測試,<include>**/*Test.java</include> 只測試以 Test.java 為文件名結(jié)尾的文件,而且不測試<exclude>**/*_Roo_*</exclude> 文件名包含_Roo_的文件。
更多的配置可以去查看maven-surefire-plugin的配置說明
- http://maven.apache.org/plugins/maven-surefire-plugin/。
在Jenkins中的使用
在Jenkins系統(tǒng)管理的插件管理頁面,添加Jenkins Emma plugin插件。
在項目配置中,加入emma:emma即可使用。
因為測試需要很長時間,而package命令會自動執(zhí)行測試,所以有時候我們不想所有項目都測試。可以使用如下方案:系統(tǒng)配置兩個分支,A分支用于開發(fā),B分支用于上線。我們希望只要A分支進行emma測試,而在B分支不用測試方便快速上線。配置如下
在項目的pom.xml中,<skipTests>false</skipTests>默認不跳過測試,在B項目中配置 clean -U compile package -Dmaven.test.skip=true ,用來跳過測試。
查看測試報告
本地測試查看:
生成的報告是以html存儲,默認的位置是${項目目錄}/target/site/emma,打開index.html可以查看。
里面有類覆蓋率,方法覆蓋率,塊覆蓋率,行覆蓋率,
選中其中的java文件還可以查看具體的代碼覆蓋率
綠色為有測試的,紅色的是測試未覆蓋的。
Jenkins 測試查看:
在項目主頁中查看
這里會有項目的測試覆蓋率曲線。x軸是版本變化,y軸是測試覆蓋率。
點進圖片進入本版本的詳細測試報告。具體的形式和本地測試報告差不多,只是 jenkins測試報告沒有具體的代碼測試詳情。
工作原理
emma現(xiàn)在有兩種工作方式,on-the-fly模式和offline模式:
On the fly 模式往加載的類中加入字節(jié)碼,在程序運行中,用 EMMA 實現(xiàn)的classLoader 替代應(yīng)用默認的 Custom classLoader,動態(tài)加載類文件,并向類中加入一些統(tǒng)計測試的字節(jié)碼,這樣運行結(jié)束后,測試數(shù)據(jù)也就通過這些臨時加入的字節(jié)碼分析出來。
Offline 模式在類被加載前,在編譯生成的class文件中加入字節(jié)碼。
On the fly 模式比較方便,缺點也比較明顯:
它不能為被 boot class loader 加載的類生成覆蓋率報告;而且,J2EE的classLoader和EMMA的classLoader都是同一類Custom classLoader,在j2ee項目啟動過程中,必須選擇應(yīng)用容器(tomcat、Weblogic等等)相應(yīng)的classLoader,從而無法使用emma的classLoader。同時,jenkins必須配合mvn的框架才能運行emma相關(guān)命令,而mvn框架只支持offline模式,所以如果想使用jenkins來做測試報告的話,就無法使用on the fly模式。
在官方文檔里也有說明:
As convenient as the on-the-fly mode is, in many cases it is not sufficient. For example, running a commercial J2EE container in a custom instrumenting classloader is practically impossible. Certain (bad) coding practices also fail for code executing in a custom classloader. This on-the-fly instrumentation mode is handy for light-weight testing of main() test methods, individual classes, and small- to- mid-size programs. emmarun also works well with Swing applications.
這時,我們只能求助于 Offline 模式。下面用maven的運行方式來介紹一下。
通過在maven中執(zhí)行,我們可以看出emma工作時主要運行以下幾個步驟
插裝字節(jié)碼
emma執(zhí)行是最重要的就是插裝字節(jié)碼:
emma循環(huán)調(diào)用handleFile()方法來遍歷目錄下所有以’.class’結(jié)尾的文件,然后使用 classParser類得到要插裝的組件
ClassDef class_table () throws IOException {m_table = new ClassDef ();magic ();version ();if (DEBUG) System.out.println (s_line);constant_pool ();if (DEBUG) System.out.println (s_line);access_flags ();this_class ();super_class ();if (DEBUG) System.out.println (s_line);//得到所有接口interfaces ();if (DEBUG) System.out.println (s_line);//得到所有字段fields ();if (DEBUG) System.out.println (s_line);//得到所有方法methods ();if (DEBUG) System.out.println (s_line);//得到所有attributeattributes ();if (DEBUG) System.out.println (s_line);return m_tabl e; }offline模式的插裝會生成全新的class文件,默認放在target/generated-classes下。以下是原java文件和插裝后的class反編譯的java文件。
public class EmmaMain2 {private Logger logger = LoggerFactory.getLogger(this.getClass());//junit調(diào)用publicTest()進行測試public void publicTest(){logger.info("this is a public method");logger.info("我是分隔符------------------------------------------------------------------");for(int i =1 ;i<10;i++){privateTest();if(i==4){continue;}if(i==3){break;}//永遠不會執(zhí)行到這一步,所以protectedTest()并沒有被覆蓋if(i==5){protectedTest();return;}}}protected void protectedTest(){logger.info("this is a protected method");}private void privateTest(){logger.info("this is a private method");} }插裝字節(jié)碼之后反編譯的代碼
public class EmmaMain2 {private Logger logger = LoggerFactory.getLogger(getClass());private static final boolean[][] $VRc;private static final long serialVersionUID = -6204774612524021426L;public EmmaMain2(){arrayOfBoolean[0] = true;}public void publicTest(){boolean[][] tmp3_0 = $VRc; if (tmp3_0 == null) tmp3_0; boolean[] arrayOfBoolean = $VRi()[1]; this.logger.info("this is a public method");this.logger.info("我是分隔符------------------------------------------------------------------");int i = 1; arrayOfBoolean[0] = true;tmpTernaryOp = tmp3_0;do{privateTest();arrayOfBoolean[1] = true; if (i == 4) { arrayOfBoolean[2] = true;} else{arrayOfBoolean[3] = true; if (i == 3) { arrayOfBoolean[4] = true;break;}arrayOfBoolean[5] = true; if (i == 5) {protectedTest(); arrayOfBoolean[6] = true;return;}}i++; arrayOfBoolean[7] = true; arrayOfBoolean[8] = true; } while (i < 10);arrayOfBoolean[9] = true;}protected void protectedTest(){boolean[][] tmp3_0 = $VRc; if (tmp3_0 == null) tmp3_0; boolean[] arrayOfBoolean = $VRi()[2]; this.logger.info("this is a protected method");arrayOfBoolean[0] = true;}private void privateTest(){boolean[][] tmp3_0 = $VRc; if (tmp3_0 == null) tmp3_0; boolean[] arrayOfBoolean = $VRi()[3]; this.logger.info("this is a private method");arrayOfBoolean[0] = true;}static{boolean[] arrayOfBoolean = $VRi()[4];arrayOfBoolean[0] = true;}private static boolean[][] $VRi(){boolean[][] tmp9_6 = (EmmaMain2.$VRc = new boolean[5]);tmp9_6[0] = new boolean[1];boolean[][] tmp15_9 = tmp9_6;tmp15_9[1] = new boolean[10];boolean[][] tmp22_15 = tmp15_9;tmp22_15[2] = new boolean[1];boolean[][] tmp28_22 = tmp22_15;tmp28_22[3] = new boolean[1];boolean[][] tmp34_28 = tmp28_22;tmp34_28[4] = new boolean[1];boolean[][] tmp40_34 = tmp34_28;//將類信息加載到內(nèi)存中。RT.r(tmp40_34, "com/impulse/test/emma/EmmaMain2", -5598510326399570528L);return tmp40_34;} }反編譯有些問題,但我們可以看出,emma在每個方法的入口和出口和轉(zhuǎn)移指令之前如return、break、continue都加入了監(jiān)測代碼,并在最后把代碼的執(zhí)行情況通過RT.r()方法加載到內(nèi)存的m_coverageMap中。
public static void r (final boolean [][] coverage, final String classVMName, final long stamp){// note that we use class names, not the actual Class objects, as the keys here. This// is not the best possible solution because it is not capable of supporting// multiply (re)loaded classes within the same app, but the rest of the toolkit// isn't designed to support this anyway. Furthermore, this does not interfere// with class unloading.final ICoverageData cdata = getCoverageData (); // need to use accessor for JMM reasons// ['cdata' can be null if a previous call to dumpCoverageData() disabled data collection]if (cdata != null){synchronized (cdata.lock ()){// TODO: could something useful be communicated back to the class// by returning something here [e.g., unique class ID (solves the// issues of class name collisions and class reloading) or RT.class// (to prevent RT reloading)]cdata.addClass (coverage, classVMName, stamp);}}}public void addClass (final boolean [][] coverage, final String classVMName, final long stamp){m_coverageMap.put (classVMName, new DataHolder (coverage, stamp));}所以當(dāng)我們只測試publicTest()時,雖然publicTest()調(diào)用了protectedTest(),但由于我們通過條件語句的控制,使得protectedTest()永遠不會被執(zhí)行,因此在轉(zhuǎn)移指令時加監(jiān)控是必要的,我們可以在生成的報告中看出,
emma能夠檢測出那些雖然調(diào)用但沒有執(zhí)行到的代碼。
收集覆蓋率信息
emma會檢測jvm的運行情況,當(dāng)通過命令行調(diào)用reset或者虛擬機停止(一般是測試完成時),emma會將測試的覆蓋率信息通過 dumpCoverageData()方法導(dǎo)出成實體文件。默認為coverage-*.ec文件。
static void dumpCoverageData (final ICoverageData cdata, final boolean useSnapshot,final File outFile, final boolean merge){try{if (cdata != null){// use method-scoped loggers everywhere in RT:final Logger log = Logger.getLogger ();final boolean info = log.atINFO ();final long start = info ? System.currentTimeMillis () : 0;{final ICoverageData cdataView = useSnapshot ? cdata.shallowCopy () : cdata;synchronized (Object.class) // fake a JVM-global critical section when multilply loaded RT's write to the same file{//在這里生者覆蓋率信息文件,cdataView是CoverageData型,有一個重要的成員變量就是上面說的m_coverageMapDataFactory.persist (cdataView, outFile, merge);}}if (info){final long end = System.currentTimeMillis ();log.info ("runtime coverage data " + (merge ? "merged into" : "written to") + " [" + outFile.getAbsolutePath () + "] {in " + (end - start) + " ms}");}}}catch (Throwable t){// logt.printStackTrace ();// TODO: do better chaining in JRE 1.4+throw new RuntimeException (IAppConstants.APP_NAME + " failed to dump coverage data: " + t.toString ());}}DataFactory.persist (cdataView, outFile, merge); cdataView是CoverageData型,有一個重要的成員變量就是上面說的m_coverageMap,沒錯,就是在這里把存在內(nèi)存中的測試覆蓋率信息持久化到文件中。
生成測試報告
AbstractReportGenerator是個抽象工廠,根據(jù)參數(shù)不同而產(chǎn)生不同的 ReportGenerator。
public static IReportGenerator create (final String type){if ((type == null) || (type.length () == 0))throw new IllegalArgumentException ("null/empty input: type");// TODO: proper pluggability pattern hereif ("html".equals (type))return new com.vladium.emma.report.html.ReportGenerator ();else if ("txt".equals (type))return new com.vladium.emma.report.txt.ReportGenerator ();else if ("xml".equals (type))return new com.vladium.emma.report.xml.ReportGenerator ();else // TODO: error codethrow new EMMARuntimeException ("no report generator class found for type [" + type + "]");} public abstract class AbstractItemVisitor implements IItemVisitor {// public: ................................................................//概要覆蓋信息public Object visit (final AllItem item, final Object ctx){return ctx;}//包測試覆蓋信息public Object visit (final PackageItem item, final Object ctx){return ctx;}//源文件測試覆蓋信息public Object visit (final SrcFileItem item, final Object ctx){return ctx;}//在html中沒有public Object visit (final ClassItem item, final Object ctx){return ctx;}//在html沒有public Object visit (final MethodItem item, final Object ctx){return ctx;} }三種ReportGenerator都實現(xiàn)了IReportGenerator接口的process方法來到處報告,而process方法又分別調(diào)用了各種重載的visit()方法。當(dāng)maven生成html測試報告是,只用了生成概要覆蓋信息、源文件測試覆蓋信息、包測試覆蓋信息的方法。
參考資料
- emma官方網(wǎng)站
- 使用 EMMA 測量測試覆蓋率
- 可愛的EMMA:測試覆蓋率工具
總結(jié)
- 上一篇: 阿里蚂蚁金服中间件(Java 4轮面试题
- 下一篇: Storm 的可靠性保证测试