EasyMock+PowerMock+Cobertura實作單元測試
EasyMock:單元測試就是對軟體的一個單元進行隔離測試,然而大多數軟體的各個單元并不是孤立,它們互相協作,有着千絲萬縷的聯系,是以為了對一個單元進行測試,我們就必須對這個單元依賴的其他單元進行模拟。
使用過程:
加入依賴:
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.2</version>
<exclusions>
<exclusion>
<artifactId>objenesis</artifactId>
<groupId>org.objenesis</groupId>
</exclusion>
</exclusions>
</dependency>
一般我們先建立一個Mock對象:
然後錄制這個對象的行為:設定方法的參數,并且設定傳回值,
激活錄制:
之後我們執行需要測試的業務代碼并檢驗代碼是否傳回正确的結果:
驗證整個過程中,Mock對象是否完成了record階段的設定,mock方法在測試過程中沒有得到執行就會報錯:
EasyMock遇到的問題:
*在mock完對象後,調用測試方法時報錯空指針異常:
分析:在測試方法中需要調用mock的對象,而測試類并沒有掃描配置檔案,mock的對象也沒有注入spring工廠中。是以調用時mock對象為null->空指針異常
解決:在測試方法的最後為mock對象寫一個set方法,在錄制完mock對象的行為後,把該對象set進測試主體對象中。
*java.lang.AssertionError: Unexpected method call …
在用
req.setSource("01");
req.setProjectId("11");
EasyMock.expect(projectBiz.getProject(req)).andReturn(result);
錄制對象行為時,不要在方法入參時填入具體的參數,而是以
這樣更加靈活的參數比對
*遇到void方法時需要使用EasyMock.expectLastCall();
模闆:redisUtil為mock出的對象
redisUtil.releaseLock(EasyMock.anyString());
EasyMock.expectLastCall();
PowerMock:PowerMock是一個擴充了其它如EasyMock等mock架構的、功能更加強大的架構。PowerMock使用一個自定義類加載器和位元組碼操作來模拟靜态方法,構造函數,final類和方法,私有方法,去除靜态初始化器等等。通過使用自定義的類加載器,簡化采用的IDE或持續內建伺服器不需要做任何改變。熟悉PowerMock支援的mock架構的開發人員會發現PowerMock很容易使用,因為對于靜态方法和構造器來說,整個的期望API是一樣的。PowerMock旨在用少量的方法和注解擴充現有的API來實作額外的功能。
使用過程:
加入依賴:
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>1.4.12</version>
<exclusions>
<exclusion>
<artifactId>javassist</artifactId>
<groupId>javassist</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>1.4.12</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-easymock</artifactId>
<version>1.4.12</version>
</dependency>
添加兩個注解,@PrepareForTest中的類為模闆靜态方法所在的類,該注解也可以放在具體的方法上:
@RunWith(PowerMockRunner.class)
@PrepareForTest( { YourClassWithEgStaticMethod.class })
Mock靜态方法:
錄制行為:
PowerMock遇到的問題:
第一個問題:java.lang.ClassFormatError
解決包依賴,整理maven的依賴關系,理清複雜的依賴關系 ,清理一些多餘的依賴包,糾正錯誤的依賴關系;
第二個問題:java.lang.VerifyError: Inconsistent stackmap frames at branch target
jvm參數 :java.lang.VerifyError: Inconsistent stackmap frames at branch target ?
原因-PowerMock中為支援對構造函數的測試,借助于Javassist實作對位元組碼的操作。但是從Java 6開始引入的Stack Map Frames特性與Javassist不相容。在Java 6中該Stack Map Frames還是可選的。但是到了Java 7,該Stackmap Frames已經是預設使用的,是以不相容問題導緻了該異常。
修改JVM 7參數:-XX:-UseSplitVerifier
->配置jvm運作參數屏蔽該特性:Run-Edit configurations-VM options選中方法後填入 -XX:-UseSplitVerifier
第三個問題:Could not reconfigure JMX java.lang.LinkageError
->忽略powermock異常:在類上加注釋:@PowerMockIgnore({"javax.management."})
---->問題解決
*PowerMock測試時待測試類初始化時自帶靜态方法導緻初始化報錯:
class A{
private HttpClientPool = HttpClientPool.getInstance();
}
->在類上加注釋:
@RunWith(PowerMockRunner.class)
@PrepareForTest({MongoHandler.class,HttpClientPool.class}) //getInstance()所在的類
@PowerMockIgnore({“javax.management.*”})
mock該類:
PowerMockito.mockStatic(HttpClientPool.class);
---->問題解決
PoewrMock定義方法行為時,mock的靜态方法傳回值都為空:
->發現在入參中使用了Easy.any(),可能是導緻該問題的原因。
又重新嘗試後,發現隻要靜态方法參數在預設情況下不比對都會傳回null;
然後開始查找入參的任意比對方式:
為了實作更加靈活的參數比對嘗試後發現可以使用例如Mockito.anyString()這樣的入參方式問題得以解決,就類似于EasyMock.anyString();
Mockito.any(BasicDBObject.class)類似于EasyMock.anyObject(TicketProjectReq.class)
*多次調用傳回不同值的問題
在測試projectListFromMongo時,在第一次查詢數量為0時,會再次調用mock的方法再次查詢,為了使第二次查詢的時候傳回數量>0:
->查找使得mock方法多次調用傳回不同值的方式:
PowerMockito.when(MongoHandler.getquerProjectResult()).thenReturn(bo).thenReturn(bo_b);
這樣在第一次調用getquerProjectResult()時傳回bo,第二次調用getquerProjectResult()時傳回bo_b;
*static void方法的mock的問題:
這實際上是一個static void方法的mock,對于這種方法,我們不需要對具體的方法做mock,隻需要mock整個類,這樣調用該方法的時候就不會跑真正的方法。其實對于static void方法最好的是什麼都不做。
-> PowerMockito.mockStatic(TransactionSynchronizationManager.class);
Cobertura :Cobertura 是一種開源工具,它通過檢測基本的代碼,并觀察在測試包運作時執行了哪些代碼和沒有執行哪些代碼,來測量測試覆寫率。除了找出未測試到的代碼并發現 bug 外,Cobertura 還可以通過标記無用的、執行不到的代碼來優化代碼,還可以提供 API 實際操作的内部資訊。
使用過程:
->在pom中添加:
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>cobertura-maven-plugin</artifactId>
<version>2.5.1</version>
</plugin>
->将項目重新打包
->點選maven的Excute Maven Goal,輸入指令:mvn cobertura:cobertura
->完成後,會在項目的目錄target下有個site檔案夾,我們打開裡面的index頁面,可以看到詳細的覆寫率以及代碼執行次數等。
Cobertura 遇到的問題:
*生成的測試覆寫率報告中,發現覆寫率為零:
閱讀日志了解:
[INFO] >>> cobertura-maven-plugin:2.5.1:cobertura (default-cli) > [cobertura]test @ ticketDubbo-internet >>>
->cobertura-maven-plugin綁定到了maven生命周期test上.
[INFO] --- cobertura-maven-plugin:2.5.1:instrument (default-cli) @ ticketDubbo-internet ---
[INFO] Instrumentation was successful.
->完成項目源碼編譯後,執行了cobertura:instrument,對項目源碼做了标記.
問題可能出在這裡:沒有複制、編譯測試資源,測試被跳過
[INFO] NOT adding cobertura ser file to attached artifacts list.
[INFO]
[INFO] --- maven-resources-plugin:2.6:testResources (default-testResources) @ ticketDubbo-internet ---
[INFO] Not copying test resources
[INFO]
[INFO] --- maven-compiler-plugin:2.5.1:testCompile (default-testCompile) @ ticketDubbo-internet ---[INFO] Not compiling test sources
[INFO]
[INFO] --- maven-surefire-plugin:2.7.2:test (default-test) @ ticketDubbo-internet ---
[INFO] Tests are skipped.
->嘗試運作時使用網上搜尋的方法cobertura:cobertura -Dmaven.test.skip=false 也沒有成功,還是 Not compiling test sources;
->仔細閱讀日志發在運作指令cobertura:cobertura過程中,它會使用maven-compiler-plugin去編譯代碼,使用maven-surefire-plugin去執行測試資源。
->maven編譯時:maven-compiler-plugin用來編譯Java代碼,src/main/java和src/test/java 兩個目錄中的所有*.java檔案會分别在comile和test-comiple階段被編譯,編譯結果分别放到了target/classes和target/test-classes目錄中
->繼續看日志:
[INFO] --- maven-compiler-plugin:2.5.1:compile (default-compile) @ ticketDubbo-internet ---
[INFO] Compiling 59 source files to E:\migumusic\ecosp-ticket-center-internet\ticketDubbo-internet\target\classes
cobertura編譯項目時隻是進行了comile階段沒有進行test-comiple階段,target/也沒有發現test-classes,是以這應該是沒有編譯單元測試導緻了結果中測試覆寫率為0的原因。
->嘗試在maven視窗手動運作compiler:testCompile:
[INFO] --- maven-compiler-plugin:2.5.1:testCompile (default-cli) @ ticketDubbo-internet ---
[INFO] Not compiling test sources
發現還是沒有編譯編譯測試代碼。
->是以應該和maven-compiler-plugin這插件有關,然後同時對maven-surefire-plugin、maven-compiler-plugin兩個插件進行學習
->maven-compiler-plugin插件了解:
maven是個項目管理工具,如果我們不告訴它我們的代碼要使用什麼樣的jdk版本編譯的話,它就會用maven-compiler-plugin預設的jdk版本來進行處理,這樣就容易出現版本不比對的問題,以至于可能導緻編譯不通過的問題。
例如代碼中要是使用上了jdk1.7的新特性,但是maven在編譯的時候使用的是jdk1.6的版本,那這一段代碼是完全不可能編譯成.class檔案的。
<source>1.6</source> <!-- 源代碼使用的開發版本 -->
<target>1.6</target> <!-- 需要生成的目标class檔案的編譯版本 -->
<!-- 一般而言,target與source是保持一緻的,但是,有時候為了讓程式能在其他版本的jdk中運作(對于低版本目标jdk,源代碼中需要沒有使用低版本jdk中不支援的文法),會存在target不同于source的情況 -->
<encoding>UTF8</encoding>
windows預設使用GBK編碼,java項目經常編碼為utf8,也需要在compiler插件中指出,否則中文亂碼可能會出現編譯錯誤。
->maven-surefire-plugin插件了解:
Maven本身并不是一個單元測試架構,Java世界中主流的單元測試架構為JUnit和TestNG。Maven所做的隻是在建構執行到特定生命周期階段的時候,通過插件來執行JUnit或者TestNG的測試用例。這一插件就是maven-surefire-plugin,可以稱之為測試運作器(Test Runner),他能很好的相容JUnit 3、JUnit 4以及TestNG。生命周期階段需要綁定到某個插件的目标才能完成真正的工作,test階段正是與maven-surefire-plugin的test目标相綁定了,這是一個内置的綁定。
在預設情況下,maven-surefire-plugin的test目标會自動執行測試源碼路徑(預設為src/test/java/)下所有符合一組命名模式的測試類。這組模式為:
*/Test.java:任何子目錄所有命名以Test開頭的Java類。
***TestCase.java:任何子目錄下所有命名以TestCase結尾的Java類。
隻要将測試類按上述模式命名,Maven就能自動運作他們,使用者也就不再需要定義測試集合(TestSuite)來聚合測試用例(TestCase)。關于模式需要注意的是,以Test結尾的測試類是不會得以自動執行的。
->回來後在parent的pom中找到了這兩個插件,都發現了true元素。
其中maven-compiler-plugin中的
<skin>true</skin>
表示不執行測試用例,也不編譯測試用例類。maven-surefire-plugin中的
<skin>true</skin>
表示不執行測試用例;
->于是它們都改為false,重新install,再次運作cobertura:cobertura時發現日志中已經有了test的編譯和運作資訊:
[INFO] --- maven-surefire-plugin:2.7.2:test (default-test) @ ticketDubbo-internet ---
[INFO] Surefire report directory: E:\migumusic\ecosp-ticket-center-internet\ticketDubbo-internet\target\surefire-reports
-------------------------------------------------------
T E S T S
-------------------------------------------------------
Running com.sitech.miso.ecosp.ticketdubbo.core.biz.OrderBizImplTest
Tests run: 4, Failures: 0, Errors: 4, Skipped: 0, Time elapsed: 0.09 sec <<< FAILURE!
Running com.sitech.miso.ecosp.ticketdubbo.core.biz.ProjectBizImplTest
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.378 sec <<< FAILURE!
Running com.sitech.miso.ecosp.ticketdubbo.core.biz.TicketBizImplTest
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.182 sec <<< FAILURE!
Running com.sitech.miso.ecosp.ticketdubbo.core.biz.YlHttpBizImplTest
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.136 sec <<< FAILURE!
Running com.sitech.miso.ecosp.ticketdubbo.core.service.OrderServiceImplTest
Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.114 sec <<< FAILURE!
Running com.sitech.miso.ecosp.ticketdubbo.core.service.TicketServiceImplTest
Tests run: 7, Failures: 0, Errors: 7, Skipped: 0, Time elapsed: 0.473 sec <<< FAILURE!
->但是又發現運作中報錯了,檢視報錯報告是之前遇到過的java.lang.VerifyError: Inconsistent stackmap frames at branch target
->解決該問題的辦法是修改方法運作時的VM options
->查找在maven-surefire-plugin運作單元測試時修改VM options的方式:
嘗試1:直接修改idea的VM Options:Help->Editcustom 加入-XX:-UseSplitVerifier,無效
嘗試2:因為它是用maven的插件來跑的單元測試,修改maven-Runner中的VMoptions,無效
嘗試3:直接修改maven-surefire-plugin插件的配置參數,搜尋了很久終于找到,在pom的配置中加入:
<argLine>-XX:-UseSplitVerifier</argLine>
,再次重新運作,日志發現添加的VM參數已經被帶入,Test運作成功。
運作cobertura:cobertura,打開報告,覆寫率生成成功。
*手動運作單元測試時正常,但是在maven-surefire-plugin運作單元測試時出現:java.lang.OutOfMemoryError: PermGen space
->PermGen space的全稱是Permanent Generation space,是指記憶體的永久儲存區域,這塊記憶體主要是被JVM存放Class和Meta資訊的,Class在被Loader時就會被放到PermGen space中, 它和存放類執行個體(Instance)的Heap區域不同,GC(Garbage Collection)不會在主程式運作期對PermGen space進行清理,是以如果你的應用中有很多CLASS的話,就很可能出現PermGen space錯誤,而maven-surefire-plugin運作單元測試時是單獨fork一個程序去做單元測試,當測試資源達到一定量時易導緻記憶體溢出。
->需要手動設定MaxPermSize的大小:
修改該插件的運作參數,在pom配置檔案中加入
<argLine>-XX:-UseSplitVerifier -XX:MaxPermSize=1024m</argLine>
後解決問題。