天天看點

EasyMock+PowerMock+Cobertura實作單元測試

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>

後解決問題。

繼續閱讀