1 主流的單元測試技術
當前互聯網主流的單元測試技術主要有Junit, JMock, Mockito ,PowerMock等框架。
1.1 Junit單元測試框架
Junit是一套java編寫的開源測試框架IDEA,Eclipse等開發工具均有集成。可以輕鬆的完成依賴關係少或者比較簡單的單元測試,但是對於依賴關係多的場景又非常耗時。比如依賴數據庫,依賴地圖資源,或者依賴第三方的某些組件。很多時候我們需要修改依賴的資源來模擬程序運行的一些場景,卻又無法保證這些依賴資源的正確性。在SpringMVC框架中我們一般只需要測試Controller層和Service即可保證比較高的測試覆蓋率。也能夠測試到程序的主要執行邏輯。爲了擺脫依賴資源對單元測試效率的鉗制,希望能夠直接模擬這些資源,就引入了Mock(模擬)框架。
1.2 Mock類框架對比
JMock既可以模擬一般方法也可以模式靜態方法等,但是寫法較爲複雜學習成本較高。Mockito 相較於JMock寫法簡單卻不能模擬靜態方法。PowerMock是Mockito 的增強,結合了兩者的優點,引入了靜態方法的Mock特性。卻因爲使用@PrepareForTest註解注入靜態方法導致生成的字節碼和一般測試字節碼有所區別,所以被註解的進來的class無法出現覆蓋率。但是我們一般不會在被測試目標中寫靜態方法,一般會寫在工具類中。所以這些靜態方法不會被覆蓋,覆蓋率會有所降低。當然這三個工具各有利弊,根據需要選擇即可。
2 如何使用PowerMock進行單元測試
接下來以Maven項目爲例手把手教你搭建PowerMock單元測試demo。
2.1 引入依賴jar包
在pom文件中添加如下所示的內容:
版本號:
<powermock.version>1.7.1</powermock.version>
<maven.compiler.source>1.6</maven.compiler.source>
<maven.compiler.target>1.6</maven.compiler.target>
<mockito1.version>1.10.19</mockito1.version>
<mockito2.version>2.8.9</mockito2.version>
<assertj-core.version>3.5.2</assertj-core.version>
<junit.version>4.12</junit.version>
引入jar:
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>${assertj-core.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-api-mockito</artifactId>
<version>${powermock.version}</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-core</artifactId>
<version>${powermock.version}</version>
</dependency>
<dependency>
<groupId>org.powermock</groupId>
<artifactId>powermock-module-junit4</artifactId>
<version>${powermock.version}</version> </dependency>
<dependency>
2.2 編寫待測試類
拿我最喜歡的餐館採購功能舉例子,我有一個通過食物種類來獲取餐館採購的食物訂單詳情的方法。cookerService.getFoodList查詢到蔬菜的採購訂單詳情,下面的循環中又將採購種類修改成了水果(真實情況應該沒有這種業務,這裏爲了說明問題)。其中RestuarantUtils.isFood是一個靜態方法。
@GetMapping("foodList")
@ResponseBody
public List<FoodResponseEntity> getFoodList(FoodRequestEntity food) {
FoodResponseEntity foodResponse = new FoodResponseEntity();
List<FoodResponseEntity> FoodList = cookerService.getFoodList("蔬菜");
if(RestuarantUtils.isFood(FoodList)){
for(FoodResponseEntity foodResponseEntity:FoodList){
foodResponseEntity.setFoodKinds("水果");
}
}
return FoodList;
}
2.3 編寫單元測試
第一步:引入PowerMockRunner並且將待Mock的靜態類必須寫到@PrepareForTest中。不要講測試對象寫到該註解,否則jacoco無法做到覆蓋率統計。
@RunWith(PowerMockRunner.class)
@PrepareForTest({RestuarantUtils.class})
public class CookerControllerTest {
第二步:注入測試對象
@InjectMocks
CookerController cookerController;
第三步:註解的形式注入模擬的普通對象
@Mock
CookerServiceImpl cookerService;
第四步:必須對註解對象和靜態Mock對象初始化
@Before
public void setUp() {
// 對定義了註解對象進行初始化
MockitoAnnotations.initMocks(this);
PowerMockito.mockStatic(RestuarantUtils.class);
}
第五步:編寫測試邏輯
首先分析要做的操作首先是Mock一個cookerService.getFoodList("蔬菜")的結果,其次是Mock靜態函數RestuarantUtils.isFood(FoodList)的結果。然後執行整個方法獲取結果。然後再和我們預期的結果對比。
@Test
public void testGetFoodList(){
//假如食物清單全部爲蔬菜
List<FoodResponseEntity> mockFoodList=new ArrayList<>();
FoodResponseEntity foodResponseEntity=new FoodResponseEntity();
foodResponseEntity.setFoodKinds("蔬菜");
mockFoodList.add(foodResponseEntity);
//現在開始Mock這個方法
PowerMockito.when(cookerService.getFoodList("蔬菜")).thenReturn(mockFoodList);
//mock靜態方法
PowerMockito.when(RestuarantUtils.isFood(mockFoodList)).thenReturn(true);
//執行完要返回的清單
List<FoodResponseEntity> returnFoodList=cookerController.getFoodList(null);
Assert.assertEquals(returnFoodList.get(0).getFoodKinds(),"水果");
}
小結:單元測試結果如下測試通過。
3 單元測試覆蓋率
3.1 maven引入覆蓋率插件
通過maven引入maven-compiler-plugin插件執行編譯操作,maven-surefire-plugin和jacoco-maven-plugin配合生成覆蓋率。
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>${jdk.version}</source>
<target>${jdk.version}</target>
<encoding>${project.build.sourceEncoding}</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.12.4</version>
<configuration>
<skip>false</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco.version}</version>
<executions>
<execution>
<id>prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>report</id>
<phase>prepare-package</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>post-unit-test</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
3.2 執行mvn生成覆蓋率文件
在terminal執行mvn clean install 生成測試文件以及覆蓋率文件。如下圖所示在瀏覽器端可以查看生成的靜態覆蓋率html文件:
(1)執行mvn clean install命令
(2)查看生成的覆蓋率文件
(3)瀏覽器查看
3.3 注意事項
引入的插件放在在<build>/<plugins>/下,不要在子級pom文件中出現pluginManagement否則mvn clean install命令不會執行覆蓋率插件。
(1)爲什麼網絡上有些人生成覆蓋率配置的命令非常長,本來mvn clean install就能實現的事情,命令中還要出現jar包?
答:這是因爲有些pom文件忽視了plugins和pluginManagement的區別。plugins 下的 plugin 是真實使用的,而 pluginManagement 下的 plugins 下的 plugin 則僅僅是一種聲明,子項目中可以對 pluginManagement 下的 plugin 進行信息的選擇、繼承、覆蓋等。
(2)執行mvn test命令爲什麼會自動執行單元測試?
約定俗成,Maven自動去尋找src/test/java下面的類,當此文件夾下面 的類符合以下規範,那麼Maven默認認爲他們是單元測試用例類。
Test*.java:任何目錄下以Test爲開始的類
*Test.java: 任何目錄下以Test爲結尾的類
*TestCase.java: 任何目錄下以TestCase爲結尾的類。
3.4 關於jacoco的在線模式和離線模式
網絡上有很多文章是對在線模式和離線模式的誤讀,這裏推薦一篇文章詳細的對比了這兩種模式,一般情況下推薦在線模式。
地址:
4 總結
經過個人的實踐和體會,推薦使用PowerMock和jacoco做單元測試。
好了,今天的教程就分享到這裏,我是快樂的一隻,一隻快樂的我。如果我的文章對你有所幫助,請隨手點個贊吧,您的鼓勵將是我堅持創作下去最大的動力。
博客地址:
https://blog.csdn.net/renchunlin66
碼雲社區地址:
https://gitee.com/renchunlin66
公衆號請搜索:“快樂的一隻”