Java單元測試技巧之PowerMock

www88xbocom17176934555x 發表於 2021-03-20

編寫Java單元測試用例,其實就是把“複雜的問題要簡單化”——即把一段複雜的程式碼拆解成一系列簡單的單元測試用例;寫好Java單元測試用例,其實就是把“簡單的問題要深入化”——即學習一套方法、總結一套模式並應用到實踐中。這裡,作者根據日常的工作經驗,總結了一些Java單元測試技巧,以供大家交流和學習。

一 準備環境

PowerMock是一個擴充套件了其它如EasyMock等mock框架的、功能更加強大的框架。PowerMock使用一個自定義類載入器和位元組碼操作來模擬靜態方法、構造方法、final類和方法、私有方法、去除靜態初始化器等等。

1 引入PowerMock包

為了引入PowerMock包,需要在pom.xml檔案中加入下列maven依賴:

<dependency>

2 整合SpringMVC專案

在SpringMVC專案中,需要在pom.xml檔案中加入JUnit的maven依賴:

<dependency>

3 整合SpringBoot專案

在SpringBoot專案中,需要在pom.xml檔案中加入JUnit的maven依賴:

<dependency>

4 一個簡單的測試用例

這裡,用List舉例,模擬一個不存在的列表,但是返回的列表大小為100。

public class ListTest {

二 mock語句

1 mock方法

宣告:

T PowerMockito.mock(Class clazz);

用途:可以用於模擬指定類的物件例項。

當模擬非final類(介面、普通類、虛基類)的非final方法時,不必使用@RunWith和@PrepareForTest註解。當模擬final類或final方法時,必須使用@RunWith和@PrepareForTest註解。註解形如:

@RunWith(PowerMockRunner.class)

@PrepareForTest({TargetClass.class})

模擬非final類普通方法

@Getter

模擬final類或final方法

@Getter

2 mockStatic方法

宣告:

PowerMockito.mockStatic(Class clazz);

用途:可以用於模擬類的靜態方法,必須使用“@RunWith”和“@PrepareForTest”註解。

@RunWith(PowerMockRunner.class)

三 spy語句

如果一個物件,我們只希望模擬它的部分方法,而希望其它方法跟原來一樣,可以使用PowerMockito.spy方法代替PowerMockito.mock方法。於是,通過when語句設定過的方法,呼叫的是模擬方法;而沒有通過when語句設定的方法,呼叫的是原有方法。

1 spy類

宣告:

PowerMockito.spy(Class clazz);

用途:用於模擬類的部分方法。

案例:

public class StringUtils {

2 spy物件

宣告:

T PowerMockito.spy(T object);

用途:用於模擬物件的部分方法。

案例:

public class StringUtils {

四 when語句

1 when().thenReturn()模式

宣告:

PowerMockito.when(mockObject.someMethod(someArgs)).thenReturn(expectedValue);

PowerMockito.when(mockObject.someMethod(someArgs)).thenThrow(expectedThrowable);

PowerMockito.when(mockObject.someMethod(someArgs)).thenAnswer(expectedAnswer);

PowerMockito.when(mockObject.someMethod(someArgs)).thenCallRealMethod();

用途:用於模擬物件方法,先執行原始方法,再返回期望的值、異常、應答,或呼叫真實的方法。

返回期望值

public class ListTest {

返回期望異常

public class ListTest {

返回期望應答

public class ListTest {

呼叫真實方法

public class ListTest {

2 doReturn().when()模式

宣告:

PowerMockito.doReturn(expectedValue).when(mockObject).someMethod(someArgs);

PowerMockito.doThrow(expectedThrowable).when(mockObject).someMethod(someArgs);

PowerMockito.doAnswer(expectedAnswer).when(mockObject).someMethod(someArgs);

PowerMockito.doNothing().when(mockObject).someMethod(someArgs);

PowerMockito.doCallRealMethod().when(mockObject).someMethod(someArgs);

用途:用於模擬物件方法,直接返回期望的值、異常、應答,或呼叫真實的方法,無需執行原始方法。

注意,千萬不要使用以下語法:

PowerMockito.doReturn(expectedValue).when(mockObject.someMethod(someArgs));

PowerMockito.doThrow(expectedThrowable).when(mockObject.someMethod(someArgs));

PowerMockito.doAnswer(expectedAnswer).when(mockObject.someMethod(someArgs));

PowerMockito.doNothing().when(mockObject.someMethod(someArgs));

PowerMockito.doCallRealMethod().when(mockObject.someMethod(someArgs));

雖然不會出現編譯錯誤,但是在執行時會丟擲UnfinishedStubbingException異常。

返回期望值

public class ListTest {

返回期望異常

public class ListTest {

返回期望應答

public class ListTest {

模擬無返回值

public class ListTest {

呼叫真實方法

public class ListTest {

3 兩種模式的主要區別

兩種模式都用於模擬物件方法,在mock例項下使用時,基本上是沒有差別的。但是,在spy例項下使用時,when().thenReturn()模式會執行原方法,而doReturn().when()模式不會執行原方法。

測試服務類

@Slf4j

使用when().thenReturn()模式

@RunWith(PowerMockRunner.class)

在測試過程中,將會列印出”呼叫獲取使用者數量方法”日誌。

使用doReturn().when()模式

@RunWith(PowerMockRunner.class)

在測試過程中,不會列印出”呼叫獲取使用者數量方法”日誌。

4 whenNew模擬構造方法

宣告:

PowerMockito.whenNew(MockClass.class).withNoArguments().thenReturn(expectedObject);

PowerMockito.whenNew(MockClass.class).withArguments(someArgs).thenReturn(expectedObject);

用途:用於模擬構造方法。

案例:

public final class FileUtils {

注意:需要加上註解@PrepareForTest({FileUtils.class}),否則模擬方法不生效。

五 引數匹配器

在執行單元測試時,有時候並不關心傳入的引數的值,可以使用引數匹配器。

1 引數匹配器(any)

Mockito提供Mockito.anyInt()、Mockito.anyString、Mockito.any(Class clazz)等來表示任意值。

public class ListTest {

2 引數匹配器(eq)

當我們使用引數匹配器時,所有引數都應使用匹配器。如果要為某一引數指定特定值時,就需要使用Mockito.eq()方法。

@RunWith(PowerMockRunner.class)

3 附加匹配器

Mockito的AdditionalMatchers類提供了一些很少使用的引數匹配器,我們可以進行引數大於(gt)、小於(lt)、大於等於(geq)、小於等於(leq)等比較操作,也可以進行引數與(and)、或(or)、非(not)等邏輯計算等。

public class ListTest {

六 verify語句

驗證是確認在模擬過程中,被測試方法是否已按預期方式與其任何依賴方法進行了互動。

格式:

Mockito.verify(mockObject[,times(int)]).someMethod(somgArgs);

用途:用於模擬物件方法,直接返回期望的值、異常、應答,或呼叫真實的方法,無需執行原始方法。

案例:

1 驗證呼叫方法

public class ListTest {

2 驗證呼叫次數

public class ListTest {

除times外,Mockito還支援atLeastOnce、atLeast、only、atMostOnce、atMost等次數驗證器。

3 驗證呼叫順序

public class ListTest {

4 驗證呼叫引數

public class ListTest {

5 確保驗證完畢

Mockito提供Mockito.verifyNoMoreInteractions方法,在所有驗證方法之後可以使用此方法,以確保所有呼叫都得到驗證。如果模擬物件上存在任何未驗證的呼叫,將會丟擲NoInteractionsWanted異常。

public class ListTest {

備註:Mockito.verifyZeroInteractions方法與Mockito.verifyNoMoreInteractions方法相同,但是目前已經被廢棄。

6 驗證靜態方法

Mockito沒有靜態方法的驗證方法,但是PowerMock提供這方面的支援。

@RunWith(PowerMockRunner.class)

七 私有屬性

1 ReflectionTestUtils.setField方法

在用原生JUnit進行單元測試時,我們一般採用ReflectionTestUtils.setField方法設定私有屬性值。

@Service

注意:在測試類中,UserService例項是通過@Autowired註解載入的,如果該例項已經被動態代理,ReflectionTestUtils.setField方法設定的是代理例項,從而導致設定不生效。

2 Whitebox.setInternalState方法

現在使用PowerMock進行單元測試時,可以採用Whitebox.setInternalState方法設定私有屬性值。

@Service

注意:需要加上註解@RunWith(PowerMockRunner.class)。

八 私有方法

1 模擬私有方法

通過when實現

public class UserService {

通過stub實現

通過模擬方法stub(存根),也可以實現模擬私有方法。但是,只能模擬整個方法的返回值,而不能模擬指定引數的返回值。

@RunWith(PowerMockRunner.class)

3 測試私有方法

@RunWith(PowerMockRunner.class)

4 驗證私有方法

@RunWith(PowerMockRunner.class)

這裡,也可以用Method那套方法進行模擬和驗證方法。

九 主要註解

PowerMock為了更好地支援SpringMVC/SpringBoot專案,提供了一系列的註解,大大地簡化了測試程式碼。

1 @RunWith註解

@RunWith(PowerMockRunner.class)

指定JUnit 使用 PowerMock 框架中的單元測試執行器。

2 @PrepareForTest註解

@PrepareForTest({ TargetClass.class })

當需要模擬final類、final方法或靜態方法時,需要新增@PrepareForTest註解,並指定方法所在的類。如果需要指定多個類,在{}中新增多個類並用逗號隔開即可。

3 @Mock註解

@Mock註解建立了一個全部Mock的例項,所有屬性和方法全被置空(0或者null)。

4 @Spy註解

@Spy註解建立了一個沒有Mock的例項,所有成員方法都會按照原方法的邏輯執行,直到被Mock返回某個具體的值為止。

注意:@Spy註解的變數需要被初始化,否則執行時會丟擲異常。

5 @InjectMocks註解

@InjectMocks註解建立一個例項,這個例項可以呼叫真實程式碼的方法,其餘用@Mock或@Spy註解建立的例項將被注入到用該例項中。

@Service

6 @Captor註解

@Captor註解在欄位級別建立引數捕獲器。但是,在測試方法啟動前,必須呼叫MockitoAnnotations.openMocks(this)進行初始化。

@Service

7 @PowerMockIgnore註解

為了解決使用PowerMock後,提示ClassLoader錯誤。

十 相關觀點

1 《Java開發手冊》規範

【強制】好的單元測試必須遵守AIR原則。說明:單元測試線上上執行時,感覺像空氣(AIR)一樣感覺不到,但在測試質量的保障上,卻是非常關鍵的。好的單元測試巨集觀上來說,具有自動化、獨立性、可重複執行的特點。

  • A:Automatic(自動化)

  • I:Independent(獨立性)

  • R:Repeatable(可重複)

【強制】單元測試應該是全自動執行的,並且非互動式的。測試用例通常是被定期執行的,執行過程必須完全自動化才有意義。輸出結果需要人工檢查的測試不是一個好的單元測試。單元測試中不準使用System.out來進行人肉驗證,必須使用assert來驗證。

【強制】單元測試是可以重複執行的,不能受到外界環境的影響。

說明:單元測試通常會被放到持續整合中,每次有程式碼check in時單元測試都會被執行。如果單測對外部環境(網路、服務、中介軟體等)有依賴,容易導致持續整合機制的不可用。

正例:為了不受外界環境影響,要求設計程式碼時就把SUT的依賴改成注入,在測試時用spring 這樣的DI框架注入一個本地(記憶體)實現或者Mock實現。

【推薦】編寫單元測試程式碼遵守BCDE原則,以保證被測試模組的交付質量。

  • B:Border,邊界值測試,包括迴圈邊界、特殊取值、特殊時間點、資料順序等。

  • C:Correct,正確的輸入,並得到預期的結果。

  • D:Design,與設計文件相結合,來編寫單元測試。

  • E:Error,強制錯誤資訊輸入(如:非法資料、異常流程、業務允許外等),並得到預期的結果。

2 為什麼要使用Mock?

根據網路相關資料,總結觀點如下:

Mock可以用來解除外部服務依賴,從而保證了測試用例的獨立性

現在的網際網路軟體系統,通常採用了分散式部署的微服務,為了單元測試某一服務而準備其它服務,存在極大的依耐性和不可行性。

Mock可以減少全鏈路測試資料準備,從而提高了編寫測試用例的速度

傳統的整合測試,需要準備全鏈路的測試資料,可能某些環節並不是你所熟悉的。最後,耗費了大量的時間和經歷,並不一定得到你想要的結果。現在的單元測試,只需要模擬上游的輸入資料,並驗證給下游的輸出資料,編寫測試用例並進行測試的速度可以提高很多倍。

Mock可以模擬一些非正常的流程,從而保證了測試用例的程式碼覆蓋率

根據單元測試的BCDE原則,需要進行邊界值測試(Border)和強制錯誤資訊輸入(Error),這樣有助於覆蓋整個程式碼邏輯。在實際系統中,很難去構造這些邊界值,也能難去觸發這些錯誤資訊。而Mock從根本上解決了這個問題:想要什麼樣的邊界值,只需要進行Mock;想要什麼樣的錯誤資訊,也只需要進行Mock。

Mock可以不用載入專案環境配置,從而保證了測試用例的執行速度

在進行整合測試時,我們需要載入專案的所有環境配置,啟動專案依賴的所有服務介面。往往執行一個測試用例,需要幾分鐘乃至幾十分鐘。採用Mock實現的測試用例,不用載入專案環境配置,也不依賴其它服務介面,執行速度往往在幾秒之內,大大地提高了單元測試的執行速度。

3 單元測試與整合測試的區別

在實際工作中,不少同學用整合測試代替了單元測試,或者認為整合測試就是單元測試。這裡,總結為了單元測試與整合測試的區別:

測試物件不同

單元測試物件是實現了具體功能的程式單元,整合測試物件是概要設計規劃中的模組及模組間的組合。

測試方法不同

單元測試中的主要方法是基於程式碼的白盒測試,整合測試中主要使用基於功能的黑盒測試。

測試時間不同

整合測試要晚於單元測試。

測試內容不同

單元測試主要是模組內程式的邏輯、功能、引數傳遞、變數引用、出錯處理及需求和設計中具體要求方面的測試;而整合測試主要驗證各個介面、介面之間的資料傳遞關係,及模組組合後能否達到預期效果。
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...
read.douban.com/reader/column/3886...

本作品採用《CC 協議》,轉載必須註明作者和本文連結