編寫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 協議》,轉載必須註明作者和本文連結