Java 程式設計技巧之單元測試用例編寫流程

Java 程式設計技巧之單元測試用例編寫流程

溫馨提示:本文較長,同學們可收藏後再看 :)

前言

清代傑出思想家章學誠有一句名言:“學必求其心得,業必貴其專精。”

意思是:學習上一定要追求心得體會,事業上一定要貴以專注精深。做技術就是這樣,一件事如果做到了極致,就必然會有所心得體會。作者最近在一個專案上,追求單元測試覆蓋率到極致,所以才有了這篇心得體會。

上一篇文章《Java單元測試技巧之PowerMock》除了介紹單元測試基礎知識外,主要介紹了“為什麼要編寫單元測試”。很多同學讀完後,還是不能快速地編寫單元測試用例。而這篇文章,立足於“如何來編寫單元測試用例”,能夠讓同學們“有章可循”,能快速地編寫出單元測試用例。

1。編寫單元測試用例

1。1。測試框架簡介

Mockito是一個單元測試模擬框架,可以讓你寫出優雅、簡潔的單元測試程式碼。Mockito採用了模擬技術,模擬了一些在應用中依賴的複雜物件,從而把測試物件和依賴物件隔離開來。

PowerMock是一個單元測試模擬框架,是在其它單元測試模擬框架的基礎上做出擴充套件。 透過提供定製的類載入器以及一些位元組碼篡改技術的應用,PowerMock實現了對靜態方法、構造方法、私有方法以及final方法的模擬支援等強大的功能。但是,正因為PowerMock進行了位元組碼篡改,導致部分單元測試用例並不被JaCoco統計覆蓋率。

透過作者多年單元測試的編寫經驗,優先推薦使用Mockito提供的功能;只有在Mockito提供的功能不能滿足需求時,才會採用PowerMock提供的功能;但是,不推薦使用影響JaCoco統計覆蓋率的PowerMock功能。在本文中,我們也不會對影響JaCoco統計覆蓋率的PowerMock功能進行介紹。

下面,將以Mockito為主、以PowerMock為輔,介紹一下如何編寫單元測試用例。

1。2。測試框架引入

為了引入Mockito和PowerMock包,需要在maven專案的pom。xml檔案中加入以下包依賴:

Java 程式設計技巧之單元測試用例編寫流程

其中,powermock。version為2。0。9,為當前的最新版本,可根據實際情況修改。在PowerMock包中,已經包含了對應的Mockito和JUnit包,所以無需單獨引入Mockito和JUnit包。

1。3。典型程式碼案例

一個典型的服務程式碼案例如下:

/** * 使用者服務類 */@Servicepublic class UserService { /** 服務相關 */ /** 使用者DAO */ @Autowired private UserDAO userDAO; /** 標識生成器 */ @Autowired private IdGenerator idGenerator; /** 引數相關 */ /** 可以修改 */ @Value(“${userService。canModify}”) private Boolean canModify; /** * 建立使用者 * * @param userCreate 使用者建立 * @return 使用者標識 */ public Long createUser(UserVO userCreate) { // 獲取使用者標識 Long userId = userDAO。getIdByName(userCreate。getName()); // 根據存在處理 // 根據存在處理: 不存在則建立 if (Objects。isNull(userId)) { userId = idGenerator。next(); UserDO create = new UserDO(); create。setId(userId); create。setName(userCreate。getName()); userDAO。create(create); } // 根據存在處理: 已存在可修改 else if (Boolean。TRUE。equals(canModify)) { UserDO modify = new UserDO(); modify。setId(userId); modify。setName(userCreate。getName()); userDAO。modify(modify); } // 根據存在處理: 已存在禁修改 else { throw new UnsupportedOperationException(“不支援修改”); } // 返回使用者標識 return userId; }}

1。4。測試用例編寫

採用Mockito和PowerMock單元測試模擬框架,編寫的單元測試用例如下:

UserServiceTest。java:

/** * 使用者服務測試類 */@RunWith(PowerMockRunner。class)public class UserServiceTest { /** 模擬依賴物件 */ /** 使用者DAO */ @Mock private UserDAO userDAO; /** 標識生成器 */ @Mock private IdGenerator idGenerator; /** 定義被測物件 */ /** 使用者服務 */ @InjectMocks private UserService userService; /** * 在測試之前 */ @Before public void beforeTest() { // 注入依賴物件 Whitebox。setInternalState(userService, “canModify”, Boolean。TRUE); } /** * 測試: 建立使用者-新 */ @Test public void testCreateUserWithNew() { // 模擬依賴方法 // 模擬依賴方法: userDAO。getByName Mockito。doReturn(null)。when(userDAO)。getIdByName(Mockito。anyString()); // 模擬依賴方法: idGenerator。next Long userId = 1L; Mockito。doReturn(userId)。when(idGenerator)。next(); // 呼叫被測方法 String text = ResourceHelper。getResourceAsString(getClass(), “userCreateVO。json”); UserVO userCreate = JSON。parseObject(text, UserVO。class); Assert。assertEquals(“使用者標識不一致”, userId, userService。createUser(userCreate)); // 驗證依賴方法 // 驗證依賴方法: userDAO。getByName Mockito。verify(userDAO)。getIdByName(userCreate。getName()); // 驗證依賴方法: idGenerator。next Mockito。verify(idGenerator)。next(); // 驗證依賴方法: userDAO。create ArgumentCaptorUserDOuserCreateCaptor = ArgumentCaptor。forClass(UserDO。class); Mockito。verify(userDAO)。create(userCreateCaptor。capture()); text = ResourceHelper。getResourceAsString(getClass(), “userCreateDO。json”); Assert。assertEquals(“使用者建立不一致”, text, JSON。toJSONString(userCreateCaptor。getValue())); // 驗證依賴物件 Mockito。verifyNoMoreInteractions(idGenerator, userDAO); } /** * 測試: 建立使用者-舊 */ @Test public void testCreateUserWithOld() { // 模擬依賴方法 // 模擬依賴方法: userDAO。getByName Long userId = 1L; Mockito。doReturn(userId)。when(userDAO)。getIdByName(Mockito。anyString()); // 呼叫被測方法 String text = ResourceHelper。getResourceAsString(getClass(), “userCreateVO。json”); UserVO userCreate = JSON。parseObject(text, UserVO。class); Assert。assertEquals(“使用者標識不一致”, userId, userService。createUser(userCreate)); // 驗證依賴方法 // 驗證依賴方法: userDAO。getByName Mockito。verify(userDAO)。getIdByName(userCreate。getName()); // 驗證依賴方法: userDAO。modify ArgumentCaptorUserDOuserModifyCaptor = ArgumentCaptor。forClass(UserDO。class); Mockito。verify(userDAO)。modify(userModifyCaptor。capture()); text = ResourceHelper。getResourceAsString(getClass(), “userModifyDO。json”); Assert。assertEquals(“使用者修改不一致”, text, JSON。toJSONString(userModifyCaptor。getValue())); // 驗證依賴物件 Mockito。verifyNoInteractions(idGenerator); Mockito。verifyNoMoreInteractions(userDAO); } /** * 測試: 建立使用者-異常 */ @Test public void testCreateUserWithException() { // 注入依賴物件 Whitebox。setInternalState(userService, “canModify”, Boolean。FALSE); // 模擬依賴方法 // 模擬依賴方法: userDAO。getByName Long userId = 1L; Mockito。doReturn(userId)。when(userDAO)。getIdByName(Mockito。anyString()); // 呼叫被測方法 String text = ResourceHelper。getResourceAsString(getClass(), “userCreateVO。json”); UserVO userCreate = JSON。parseObject(text, UserVO。class); UnsupportedOperationException exception = Assert。assertThrows(“返回異常不一致”, UnsupportedOperationException。class, () -userService。createUser(userCreate)); Assert。assertEquals(“異常訊息不一致”, “不支援修改”, exception。getMessage()); }}

userCreateVO。json:

{“name”:“test”}

userCreateDO。json:

{“id”:1,“name”:“test”}

userModifyDO。json:

{“id”:1,“name”:“test”}

透過執行以上測試用例,可以看到對原始碼進行了100%的行覆蓋。

2。測試用例編寫流程

透過上一章編寫Java類單元測試用例的實踐,可以總結出以下Java類單元測試用例的編寫流程:

Java 程式設計技巧之單元測試用例編寫流程

單元測試用例編寫流程

上面一共有3個測試用例,這裡僅以測試用例testCreateUserWithNew(測試: 建立使用者-新)為例說明。

2。1。定義物件階段

第1步是定義物件階段,主要包括定義被測物件、模擬依賴物件(類成員)、注入依賴物件(類成員)3大部分。

2。1。1。定義被測物件

在編寫單元測試時,首先需要定義被測物件,或直接初始化、或透過Spy包裝……其實,就是把被測試服務類進行例項化。

/** 定義被測物件 *//** 使用者服務 */@InjectMocksprivate UserService userService;

2。1。2。模擬依賴物件(類成員)

在一個服務類中,我們定義了一些類成員物件——服務(Service)、資料訪問物件(DAO)、引數(Value)等。在Spring框架中,這些類成員物件透過@Autowired、@Value等方式注入,它們可能涉及複雜的環境配置、依賴第三方介面服務……但是,在單元測試中,為了解除對這些類成員物件的依賴,我們需要對這些類成員物件進行模擬。

/** 模擬依賴物件 *//** 使用者DAO */@Mockprivate UserDAO userDAO;/** 標識生成器 */@Mockprivate IdGenerator idGenerator;

2。1。3。注入依賴物件(類成員)

當模擬完這些類成員物件後,我們需要把這些類成員物件注入到被測試類的例項中。以便在呼叫被測試方法時,可能使用這些類成員物件,而不至於丟擲空指標異常。

/** 定義被測物件 *//** 使用者服務 */@InjectMocksprivate UserService userService;/** * 在測試之前 */@Beforepublic void beforeTest() { // 注入依賴物件 Whitebox。setInternalState(userService, “canModify”, Boolean。TRUE);}

2。2。模擬方法階段

第2步是模擬方法階段,主要包括模擬依賴物件(引數或返回值)、模擬依賴方法2大部分。

2。2。1。模擬依賴物件(引數或返回值)

通常,在呼叫一個方法時,需要先指定方法的引數,然後獲取到方法的返回值。所以,在模擬方法之前,需要先模擬該方法的引數和返回值。

Long userId = 1L;

2。2。2。模擬依賴方法

在模擬完依賴的引數和返回值後,就可以利用Mockito和PowerMock的功能,進行依賴方法的模擬。如果依賴物件還有方法呼叫,還需要模擬這些依賴物件的方法。

// 模擬依賴方法// 模擬依賴方法: userDAO。getByNameMockito。doReturn(null)。when(userDAO)。getIdByName(Mockito。anyString());// 模擬依賴方法: idGenerator。nextMockito。doReturn(userId)。when(idGenerator)。next();

2。3。呼叫方法階段

第3步是呼叫方法階段,主要包括模擬依賴物件(引數)、呼叫被測方法、驗證引數物件(返回值)3步。

2。3。1。模擬依賴物件(引數)

在呼叫被測方法之前,需要模擬被測方法的引數。如果這些引數還有方法呼叫,還需要模擬這些引數的方法。

String text = ResourceHelper。getResourceAsString(getClass(), “userCreateVO。json”);UserVO userCreate = JSON。parseObject(text, UserVO。class);

2。3。2。呼叫被測方法

在準備好引數物件後,就可以呼叫被測試方法了。如果被測試方法有返回值,需要定義變數接收返回值;如果被測試方法要丟擲異常,需要指定期望的異常。

userService。createUser(userCreate)

2。3。3。驗證資料物件(返回值)

在呼叫被測試方法後,如果被測試方法有返回值,需要驗證這個返回值是否符合預期;如果被測試方法要丟擲異常,需要驗證這個異常是否滿足要求。

Assert。assertEquals(“使用者標識不一致”, userId, userService。createUser(userCreate));

2。4。驗證方法階段

第4步是驗證方法階段,主要包括驗證依賴方法、驗證資料物件(引數)、驗證依賴物件3步。

2。4。1。驗證依賴方法

作為一個完整的測試用例,需要對每一個模擬的依賴方法呼叫進行驗證。

// 驗證依賴方法// 驗證依賴方法: userDAO。getByNameMockito。verify(userDAO)。getIdByName(userCreate。getName());// 驗證依賴方法: idGenerator。nextMockito。verify(idGenerator)。next();// 驗證依賴方法: userDAO。createArgumentCaptorUserDOuserCreateCaptor = ArgumentCaptor。forClass(UserDO。class);Mockito。verify(userDAO)。create(userCreateCaptor。capture());

2。4。2。驗證資料物件(引數)

對應一些模擬的依賴方法,有些引數物件是被測試方法內部生成的。為了驗證程式碼邏輯的正確性,就需要對這些引數物件進行驗證,看這些引數物件值是否符合預期。

text = ResourceHelper。getResourceAsString(getClass(), “userCreateDO。json”);Assert。assertEquals(“使用者建立不一致”, text, JSON。toJSONString(userCreateCaptor。getValue()));

2。4。3。驗證依賴物件

作為一個完整的測試用例,應該保證每一個模擬的依賴方法呼叫都進行了驗證。正好,Mockito提供了一套方法,用於驗證模擬物件所有方法呼叫都得到了驗證。

// 驗證依賴物件Mockito。verifyNoMoreInteractions(idGenerator, userDAO);

3。定義被測物件

在編寫單元測試時,首先需要定義被測物件,或直接初始化、或透過Spy包裝……其實,就是把被測試服務類進行例項化。

3。1。直接構建物件

直接構建一個物件,總是簡單又直接。

UserService userService = new UserService();

3。2。利用Mockito。spy方法

Mockito提供一個spy功能,用於攔截那些尚未實現或不期望被真實呼叫的方法,預設所有方法都是真實方法,除非主動去模擬對應方法。所以,利用spy功能來定義被測物件,適合於需要模擬被測類自身方法的情況,適用於普通類、介面和虛基類。

UserService userService = Mockito。spy(new UserService());UserService userService = Mockito。spy(UserService。class);AbstractOssService ossService = Mockito。spy(AbstractOssService。class);

3。3。利用@Spy註解

@Spy註解跟Mockito。spy方法一樣,可以用來定義被測物件,適合於需要模擬被測類自身方法的情況,適用於普通類、介面和虛基類。@Spy註解需要配合@RunWith註解使用。

@RunWith(PowerMockRunner。class)public class CompanyServiceTest { @Spy private UserService userService = new UserService(); 。。。}

注意:@Spy註解物件需要初始化。如果是虛基類或介面,可以用Mockito。mock方法例項化。

3。4。利用@InjectMocks註解

@InjectMocks註解用來建立一個例項,並將其它物件(@Mock、@Spy或直接定義的物件)注入到該例項中。所以,@InjectMocks註解本身就可以用來定義被測物件。@InjectMocks註解需要配合@RunWith註解使用。

@RunWith(PowerMockRunner。class)public class UserServiceTest { @InjectMocks private UserService userService; 。。。}

4。模擬依賴物件

在編寫單元測試用例時,需要模擬各種依賴物件——類成員、方法引數和方法返回值。

4。1。直接構建物件

如果需要構建一個物件,最簡單直接的方法就是——定義物件並賦值。

Long userId = 1L;String userName = “admin”;UserDO user = new User();user。setId(userId);user。setName(userName);ListLonguserIdList = Arrays。asList(1L, 2L, 3L);

4。2。反序列化物件

如果物件欄位或層級非常龐大,採用直接構建物件方法,可能會編寫大量構建程式程式碼。這種情況,可以考慮反序列化物件,將會大大減少程式程式碼。由於JSON字串可讀性高,這裡就以JSON為例,介紹反序列化物件。

反序列化模型物件:

String text = ResourceHelper。getResourceAsString(getClass(), “user。json”);UserDO user = JSON。parseObject(text, UserDO。class);

反序列化集合物件:

String text = ResourceHelper。getResourceAsString(getClass(), “userList。json”);ListUserDOuserList = JSON。parseArray(text, UserDO。class);

反序列化對映物件:

String text = ResourceHelper。getResourceAsString(getClass(), “userMap。json”);MapLong, UserDOuserMap = JSON。parseObject(text, new TypeReferenceMapLong, UserDO() {});

4。3。利用Mockito。mock方法

Mockito提供一個mock功能,用於攔截那些尚未實現或不期望被真實呼叫的方法,預設所有方法都已被模擬——方法為空並返回預設值(null或0),除非主動執行doCallRealMethod或thenCallRealMethod操作,才能夠呼叫真實的方法。

利用Mockito。mock方法模擬依賴物件,主要用於以下幾種情形:

只使用類例項,不使用類屬性;

類屬性太多,但使用其中少量屬性(可以mock屬性返回值);

類是介面或虛基類,並不關心其具體實現類。 MockClass mockClass = Mockito。mock(MockClass。class);ListLonguserIdList = (ListLong)Mockito。mock(List。class);

4。4。利用@Mock註解

@Mock註解跟Mockito。mock方法一樣,可以用來模擬依賴物件,適用於普通類、介面和虛基類。@Mock註解需要配合@RunWith註解使用。

@RunWith(PowerMockRunner。class)public class UserServiceTest { @Mock private UserDAO userDAO; 。。。}

4。5。利用Mockito。spy方法

Mockito。spy方法跟Mockito。mock方法功能相似,只是Mockito。spy方法預設所有方法都是真實方法,除非主動去模擬對應方法。

UserService userService = Mockito。spy(new UserService());UserService userService = Mockito。spy(UserService。class);AbstractOssService ossService = Mockito。spy(AbstractOssService。class);

4。6。利用@Spy註解

@Spy註解跟Mockito。spy方法一樣,可以用來模擬依賴物件,適用於普通類、介面和虛基類。@Spy註解需要配合@RunWith註解使用。

@RunWith(PowerMockRunner。class)public class CompanyServiceTest { @Spy private UserService userService = new UserService(); 。。。}

注意:@Spy註解物件需要初始化。如果是虛基類或介面,可以用Mockito。mock方法例項化。

5。注入依賴物件

當模擬完這些類成員物件後,我們需要把這些類成員物件注入到被測試類的例項中。以便在呼叫被測試方法時,可能使用這些類成員物件,而不至於丟擲空指標異常。

5。1。利用Setter方法注入

如果類定義了Setter方法,可以直接呼叫方法設定欄位值。

userService。setMaxCount(100);userService。setUserDAO(userDAO);

5。2。利用ReflectionTestUtils。setField方法注入

JUnit提供ReflectionTestUtils。setField方法設定屬性欄位值。

ReflectionTestUtils。setField(userService, “maxCount”, 100);ReflectionTestUtils。setField(userService, “userDAO”, userDAO);

5。3。利用Whitebox。setInternalState方法注入

PowerMock提供Whitebox。setInternalState方法設定屬性欄位值。

Whitebox。setInternalState(userService, “maxCount”, 100);Whitebox。setInternalState(userService, “userDAO”, userDAO);

5。4。利用@InjectMocks註解注入

@InjectMocks註解用來建立一個例項,並將其它物件(@Mock、@Spy或直接定義的物件)注入到該例項中。@InjectMocks註解需要配合@RunWith註解使用。

@RunWith(PowerMockRunner。class)public class UserServiceTest { @Mock private UserDAO userDAO; private Boolean canModify; @InjectMocks private UserService userService; 。。。}

5。5。設定靜態常量欄位值

有時候,我們需要對靜態常量物件進行模擬,然後去驗證是否執行了對應分支下的方法。比如:需要模擬Lombok的@Slf4j生成的log靜態常量。但是,Whitebox。setInternalState方法和@InjectMocks註解並不支援設定靜態常量,需要自己實現一個設定靜態常量的方法:

public final class FieldHelper { public static void setStaticFinalField(Class?clazz, String fieldName, Object fieldValue) throws NoSuchFieldException, IllegalAccessException { Field field = clazz。getDeclaredField(fieldName); FieldUtils。removeFinalModifier(field); FieldUtils。writeStaticField(field, fieldValue, true); }}

具體使用方法如下:

FieldHelper。setStaticFinalField(UserService。class, “log”, log);

注意:經過測試,該方法對於int、Integer等基礎型別並不生效,應該是編譯器常量最佳化導致。

6。模擬依賴方法

在模擬完依賴的引數和返回值後,就可以利用Mockito和PowerMock的功能,進行依賴方法的模擬。如果依賴物件還有方法呼叫,還需要模擬這些依賴物件的方法。

6。1。根據返回模擬方法

6。1。1。模擬無返回值方法

Mockito。doNothing()。when(userDAO)。delete(userId);

6。1。2。模擬方法單個返回值

Mockito。doReturn(user)。when(userDAO)。get(userId);Mockito。when(userDAO。get(userId))。thenReturn(user);

6。1。3。模擬方法多個返回值

直接列舉出多個返回值:

Mockito。doReturn(record0, record1, record2, null)。when(recordReader)。read();Mockito。when(recordReader。read())。thenReturn(record0, record1, record2, null);

轉化列表為多個返回值:

ListRecordrecordList = 。。。;Mockito。doReturn(recordList。get(0), recordList。subList(1, recordList。size())。toArray())。when(recordReader)。read();Mockito。when(recordReader。read())。thenReturn(recordList。get(0), recordList。subList(1, recordList。size())。toArray());

6。1。4。模擬方法定製返回值

可利用Answer定製方法返回值:

MapLong, UserDOuserMap = 。。。;Mockito。doAnswer(invocation -userMap。get(invocation。getArgument(0))) 。when(userDAO)。get(Mockito。anyLong());Mockito。when(userDAO。get(Mockito。anyLong())) 。thenReturn(invocation -userMap。get(invocation。getArgument(0)));Mockito。when(userDAO。get(Mockito。anyLong())) 。then(invocation -userMap。get(invocation。getArgument(0)));

6。1。5。模擬方法丟擲單個異常

指定單個異常型別:

Mockito。doThrow(PersistenceException。class)。when(userDAO)。get(Mockito。anyLong());Mockito。when(userDAO。get(Mockito。anyLong()))。thenThrow(PersistenceException。class);

指定單個異常物件:

Mockito。doThrow(exception)。when(userDAO)。get(Mockito。anyLong());Mockito。when(userDAO。get(Mockito。anyLong()))。thenThrow(exception);

6。1。6。模擬方法丟擲多個異常

指定多個異常型別:

Mockito。doThrow(PersistenceException。class, RuntimeException。class)。when(userDAO)。get(Mockito。anyLong());Mockito。when(userDAO。get(Mockito。anyLong()))。thenThrow(PersistenceException。class, RuntimeException。class);

指定多個異常物件:

Mockito。doThrow(exception1, exception2)。when(userDAO)。get(Mockito。anyLong());Mockito。when(userDAO。get(Mockito。anyLong()))。thenThrow(exception1, exception2);

6。1。7。直接呼叫真實方法

Mockito。doCallRealMethod()。when(userService)。getUser(userId);Mockito。when(userService。getUser(userId))。thenCallRealMethod();

6。2。根據引數模擬方法

Mockito提供do-when語句和when-then語句模擬方法。

6。2。1。模擬無引數方法

對於無引數的方法模擬:

Mockito。doReturn(deleteCount)。when(userDAO)。deleteAll();Mockito。when(userDAO。deleteAll())。thenReturn(deleteCount);

6。2。2。模擬指定引數方法

對於指定引數的方法模擬:

Mockito。doReturn(user)。when(userDAO)。get(userId);Mockito。when(userDAO。get(userId))。thenReturn(user);

6。2。3。模擬任意引數方法

在編寫單元測試用例時,有時候並不關心傳入引數的具體值,可以使用Mockito引數匹配器的any方法。Mockito提供了anyInt、anyLong、anyString、anyList、anySet、anyMap、any(Class clazz)等方法來表示任意值。

Mockito。doReturn(user)。when(userDAO)。get(Mockito。anyLong());Mockito。when(userDAO。get(Mockito。anyLong()))。thenReturn(user);

6。2。4。模擬可空引數方法

Mockito引數匹配器的any具體方法,並不能夠匹配null物件。而Mockito提供一個nullable方法,可以匹配包含null物件的任意物件。此外,Mockito。any()方法也可以用來匹配可空引數。

Mockito。doReturn(user)。when(userDAO) 。queryCompany(Mockito。anyLong(), Mockito。nullable(Long。class));Mockito。when(userDAO。queryCompany(Mockito。anyLong(), MockitoLong。any())) 。thenReturn(user);

6。2。5。模擬必空引數方法

同樣,如果要匹配null物件,可以使用isNull方法,或使用eq(null)。

Mockito。doReturn(user)。when(userDAO)。queryCompany(Mockito。anyLong(), Mockito。isNull());Mockito。when(userDAO。queryCompany(Mockito。anyLong(), Mockito。eq(null)))。thenReturn(user);

6。2。6。模擬不同引數方法

Mockito支援按不同的引數分別模擬同一方法。

Mockito。doReturn(user1)。when(userDAO)。get(1L);Mockito。doReturn(user2)。when(userDAO)。get(2L);。。。

注意:如果一個引數滿足多個模擬方法條件,會以最後一個模擬方法為準。

6。2。7。模擬可變引數方法

對於一些變長度引數方法,可以按實際引數個數進行模擬:

Mockito。when(userService。delete(Mockito。anyLong())。thenReturn(true);Mockito。when(userService。delete(1L, 2L, 3L)。thenReturn(true);

也可以用Mockito。any()模擬一個通用匹配方法:

Mockito。when(userService。delete(Mockito。Longany())。thenReturn(true);

注意:Mockito。Tany()並不等於Mockito。any(ClassTtype),前者可以匹配null和型別T的可變引數,後者只能匹配T必填引數。

6。3。模擬其它特殊方法

6。3。1。模擬final方法

PowerMock提供對final方法的模擬,方法跟模擬普通方法一樣。但是,需要把對應的模擬類新增到@PrepareForTest註解中。

// 新增@PrepareForTest註解@PrepareForTest({UserService。class})// 跟模擬普通方法完全一致Mockito。doReturn(userId)。when(idGenerator)。next();Mockito。when(idGenerator。next())。thenReturn(userId);

6。3。2。模擬私有方法

PowerMock提供提對私有方法的模擬,但是需要把私有方法所在的類放在@PrepareForTest註解中。

PowerMockito。doReturn(true)。when(UserService。class, “isSuper”, userId);PowerMockito。when(UserService。class, “isSuper”, userId)。thenReturn(true);

6。3。3。模擬構造方法

PowerMock提供PowerMockito。whenNew方法來模擬構造方法,但是需要把使用構造方法的類放在@PrepareForTest註解中。

PowerMockito。whenNew(UserDO。class)。withNoArguments()。thenReturn(userDO);PowerMockito。whenNew(UserDO。class)。withArguments(userId, userName)。thenReturn(userDO);

6。3。4。模擬靜態方法

PowerMock提供PowerMockito。mockStatic和PowerMockito。spy來模擬靜態方法類,然後就可以模擬靜態方法了。同樣,需要把對應的模擬類新增到@PrepareForTest註解中。

// 模擬對應的類PowerMockito。mockStatic(HttpHelper。class);PowerMockito。spy(HttpHelper。class);// 模擬對應的方法PowerMockito。when(HttpHelper。httpPost(SERVER_URL))。thenReturn(response);PowerMockito。doReturn(response)。when(HttpHelper。class, “httpPost”, SERVER_URL);PowerMockito。when(HttpHelper。class, “httpPost”, SERVER_URL)。thenReturn(response);

注意:第一種方式不適用於PowerMockito。spy模擬的靜態方法類。

7。呼叫被測方法

在準備好引數物件後,就可以呼叫被測試方法了。

如果把方法按訪問許可權分類,可以簡單地分為有訪問許可權和無訪問許可權兩種。但實際上,Java語言中提供了public、protected、private和缺失共4種許可權修飾符,在不同的環境下又對應不同的訪問許可權。具體對映關係如下:

修飾符

本類

本包

子類

其它

public

protected

預設

private

下面,將根據有訪問許可權和無訪問許可權兩種情況,來介紹如何呼叫被測方法。

7。1。呼叫構造方法

7。1。1。呼叫有訪問許可權的構造方法

可以直接呼叫有訪問許可權的構造方法。

UserDO user = new User();UserDO user = new User(1L, “admin”);

7。1。2。呼叫無訪問許可權的構造方法

呼叫無訪問許可權的構造方法,可以使用PowerMock提供的Whitebox。invokeConstructor方法。

Whitebox。invokeConstructor(NumberHelper。class);Whitebox。invokeConstructor(User。class, 1L, “admin”);

備註:該方法也可以呼叫有訪問許可權的構造方法,但是不建議使用。

7。2。呼叫普通方法

7。2。1。呼叫有訪問許可權的普通方法

可以直接呼叫有訪問許可權的普通方法。

userService。deleteUser(userId);User user = userService。getUser(userId);

7。2。2。呼叫無許可權訪問的普通方法

呼叫無訪問許可權的普通方法,可以使用PowerMock提供的Whitebox。invokeMethod方法。

User user = (User)Whitebox。invokeMethod(userService, “isSuper”, userId);

也可以使用PowerMock提供Whitebox。getMethod方法和PowerMockito。method方法,可以直接獲取對應類方法物件。然後,透過Method的invoke方法,可以呼叫沒有訪問許可權的方法。

Method method = Whitebox。getMethod(UserService。class, “isSuper”, Long。class);Method method = PowerMockito。method(UserService。class, “isSuper”, Long。class);User user = (User)method。invoke(userService, userId);

備註:該方法也可以呼叫有訪問許可權的普通方法,但是不建議使用。

7。3。呼叫靜態方法

7。3。1。呼叫有許可權訪問的靜態方法

可以直接呼叫有訪問許可權的靜態方法。

boolean isPositive = NumberHelper。isPositive(-1);

7。3。2。呼叫無許可權訪問的靜態方法

呼叫無許可權訪問的靜態方法,可以使用PowerMock提供的Whitebox。invokeMethod方法。

String value = (String)Whitebox。invokeMethod(JSON。class, “toJSONString”, object);

備註:該方法也可以呼叫有訪問許可權的靜態方法,但是不建議使用。

8。驗證依賴方法

在單元測試中,驗證是確認模擬的依賴方法是否按照預期被呼叫或未呼叫的過程。Mockito提供了許多方法來驗證依賴方法呼叫,給我們編寫單元測試用例帶來了很大的幫助。

8。1。根據引數驗證方法呼叫

8。1。1。驗證無引數方法呼叫 Mockito。verify(userDAO)。deleteAll();

8。1。2。驗證指定引數方法呼叫 Mockito。verify(userDAO)。delete(userId);Mockito。verify(userDAO)。delete(Mockito。eq(userId));

8。1。3。驗證任意引數方法呼叫 Mockito。verify(userDAO)。delete(Mockito。anyLong());

8。1。4。驗證可空引數方法呼叫 Mockito。verify(userDAO)。queryCompany(Mockito。anyLong(), Mockito。nullable(Long。class));

8。1。5。驗證必空引數方法呼叫 Mockito。verify(userDAO)。queryCompany(Mockito。anyLong(), Mockito。isNull());

8。1。6。驗證可變引數方法呼叫

對於一些變長度引數方法,可以按實際引數個數進行驗證:

Mockito。verify(userService)。delete(Mockito。any(Long。class));Mockito。verify(userService)。delete(1L, 2L, 3L);

也可以用Mockito。any()進行通用驗證:

Mockito。verify(userService)。delete(Mockito。Longany());

8。2。驗證方法呼叫次數

8。2。1。驗證方法預設呼叫1次 Mockito。verify(userDAO)。delete(userId);

8。2。2。驗證方法從不呼叫 Mockito。verify(userDAO, Mockito。never())。delete(userId);

8。2。3。驗證方法呼叫n次 Mockito。verify(userDAO, Mockito。times(n))。delete(userId);

8。2。4。驗證方法呼叫至少1次 Mockito。verify(userDAO, Mockito。atLeastOnce())。delete(userId);

8。2。5。驗證方法呼叫至少n次 Mockito。verify(userDAO, Mockito。atLeast(n))。delete(userId);

8。2。2。驗證方法呼叫最多1次 Mockito。verify(userDAO, Mockito。atMostOnce())。delete(userId);

8。2。6。驗證方法呼叫最多n次 Mockito。verify(userDAO, Mockito。atMost(n))。delete(userId);

8。2。7。驗證方法呼叫指定n次

Mockito允許按順序進行驗證方法呼叫,未被驗證到的方法呼叫將不會被標記為已驗證。

Mockito。verify(userDAO, Mockito。call(n))。delete(userId);

8。2。8。驗證物件及其方法呼叫1次

用於驗證物件及其方法呼叫1次,如果該物件還有別的方法被呼叫或者該方法呼叫了多次,都將導致驗證方法呼叫失敗。

Mockito。verify(userDAO, Mockito。only())。delete(userId);

相當於:

Mockito。verify(userDAO)。delete(userId);Mockito。verifyNoMoreInteractions(userDAO);

8。3。驗證方法呼叫並捕獲引數值

Mockito提供ArgumentCaptor類來捕獲引數值,透過呼叫forClass(ClassTclazz)方法來構建一個ArgumentCaptor物件,然後在驗證方法呼叫時來捕獲引數,最後獲取到捕獲的引數值並驗證。如果一個方法有多個引數都要捕獲並驗證,那就需要建立多個ArgumentCaptor物件。

ArgumentCaptor的主要介面方法:

capture方法,用於捕獲方法引數;

getValue方法,用於獲取捕獲的引數值,如果捕獲了多個引數值,該方法只返回最後一個引數值;

getAllValues方法,使用者獲取捕獲的所有引數值。

8。3。1。使用ArgumentCaptor。forClass方法定義引數捕獲器

在測試用例方法中,直接使用ArgumentCaptor。forClass方法定義引數捕獲器。

Java 程式設計技巧之單元測試用例編寫流程

注意:定義泛型類的引數捕獲器時,存在強制型別轉化,會引起編譯器警告。

8。3。2。使用@Captor註解定義引數捕獲器

也可以用Mockito提供的@Captor註解,在測試用例類中定義引數捕獲器。

@RunWith(PowerMockRunner。class)public class UserServiceTest { @Captor private ArgumentCaptorUserDOuserCaptor; @Test public void testModifyUser() { 。。。 Mockito。verify(userDAO)。modify(userCaptor。capture()); UserDO user = userCaptor。getValue(); }}

注意:定義泛型類的引數捕獲器時,由於是Mockito自行初始化,不會引起編譯器告警。

8。3。3。捕獲多次方法呼叫的引數值列表

Java 程式設計技巧之單元測試用例編寫流程

8。4。驗證其它特殊方法

8。4。1。驗證final方法呼叫

final方法的驗證跟普通方法類似,這裡不再累述。

8。4。2。驗證私有方法呼叫

PowerMockito提供verifyPrivate方法驗證私有方法呼叫。

PowerMockito。verifyPrivate(myClass, times(1))。invoke(“unload”, any(List。class));

8。4。3。驗證構造方法呼叫

PowerMockito提供verifyNew方法驗證構造方法呼叫。

PowerMockito。verifyNew(MockClass。class)。withNoArguments();PowerMockito。verifyNew(MockClass。class)。withArguments(someArgs);

8。4。4。驗證靜態方法呼叫

PowerMockito提供verifyStatic方法驗證靜態方法呼叫。

PowerMockito。verifyStatic(StringUtils。class);StringUtils。isEmpty(string);

9。驗證資料物件

JUnit測試框架中Assert類就是斷言工具類,主要驗證單元測試中實際資料物件與期望資料物件一致。在呼叫被測方法時,需要對返回值和異常進行驗證;在驗證方法呼叫時,也需要對捕獲的引數值進行驗證。

9。1。驗證資料物件空值

9。1。1。驗證資料物件為空

透過JUnit提供的Assert。assertNull方法驗證資料物件為空。

Assert。assertNull(“使用者標識必須為空”, userId);

9。1。2。驗證資料物件非空

透過JUnit提供的Assert。assertNotNull方法驗證資料物件非空。

Assert。assertNotNull(“使用者標識不能為空”, userId);

9。2。驗證資料物件布林值

9。2。1。驗證資料物件為真

透過JUnit提供的Assert。assertTrue方法驗證資料物件為真。

Assert。assertTrue(“返回值必須為真”, NumberHelper。isPositive(1));

9。2。2。驗證資料物件為假

透過JUnit提供的Assert。assertFalse方法驗證資料物件為假。

Assert。assertFalse(“返回值必須為假”, NumberHelper。isPositive(-1));

9。3。驗證資料物件引用

在單元測試用例中,對於一些引數或返回值物件,不需要驗證物件具體取值,只需要驗證物件引用是否一致。

9。3。1。驗證資料物件一致

JUnit提供的Assert。assertSame方法驗證資料物件一致。

UserDO expectedUser = 。。。;Mockito。doReturn(expectedUser)。when(userDAO)。get(userId);UserDO actualUser = userService。getUser(userId);Assert。assertSame(“使用者必須一致”, expectedUser, actualUser);

9。3。1。驗證資料物件不一致

JUnit提供的Assert。assertNotSame方法驗證資料物件一致。

UserDO expectedUser = 。。。;Mockito。doReturn(expectedUser)。when(userDAO)。get(userId);UserDO actualUser = userService。getUser(otherUserId);Assert。assertNotSame(“使用者不能一致”, expectedUser, actualUser);

9。4。驗證資料物件值

JUnit提供Assert。assertEquals、Assert。assertNotEquals、Assert。assertArrayEquals方法組,可以用來驗證資料物件值是否相等。

9。4。1。驗證簡單資料物件

對於簡單資料物件(比如:基礎型別、包裝型別、實現了equals的資料型別……),可以直接透過JUnit的Assert。assertEquals和Assert。assertNotEquals方法組進行驗證。

Assert。assertNotEquals(“使用者名稱稱不一致”, “admin”, userName);Assert。assertEquals(“賬戶金額不一致”, 10000。0D, accountAmount, 1E-6D);

9。4。2。驗證簡單陣列或集合物件

對於簡單陣列物件(比如:基礎型別、包裝型別、實現了equals的資料型別……),可以直接透過JUnit的Assert。assertArrayEquals方法組進行驗證。對於簡單集合物件,也可以透過Assert。assertEquals方法驗證。

Long[] userIds = 。。。;Assert。assertArrayEquals(“使用者標識列表不一致”, new Long[] {1L, 2L, 3L}, userIds);ListLonguserIdList = 。。。;Assert。assertEquals(“使用者標識列表不一致”, Arrays。asList(1L, 2L, 3L), userIdList);

9。4。3。驗證複雜資料物件

對於複雜的JavaBean資料物件,需要驗證JavaBean資料物件的每一個屬性欄位。

UserDO user = 。。。;Assert。assertEquals(“使用者標識不一致”, Long。valueOf(1L), user。getId());Assert。assertEquals(“使用者名稱稱不一致”, “admin”, user。getName());Assert。assertEquals(“使用者公司標識不一致”, Long。valueOf(1L), user。getCompany()。getId());。。。

9。4。4。驗證複雜陣列或集合物件

對於複雜的JavaBean陣列和集合物件,需要先展開陣列和集合物件中每一個JavaBean資料物件,然後驗證JavaBean資料物件的每一個屬性欄位。

ListUserDOexpectedUserList = 。。。;ListUserDOactualUserList = 。。。;Assert。assertEquals(“使用者列表長度不一致”, expectedUserList。size(), actualUserList。size());UserDO[] expectedUsers = expectedUserList。toArray(new UserDO[0]);UserDO[] actualUsers = actualUserList。toArray(new UserDO[0]);for (int i = 0; iactualUsers。length; i++) { Assert。assertEquals(String。format(“使用者(%s)標識不一致”, i), expectedUsers[i]。getId(), actualUsers[i]。getId()); Assert。assertEquals(String。format(“使用者(%s)名稱不一致”, i), expectedUsers[i]。getName(), actualUsers[i]。getName());Assert。assertEquals(“使用者公司標識不一致”, expectedUsers[i]。getCompany()。getId(), actualUsers[i]。getCompany()。getId()); 。。。}

9。4。5。透過序列化驗證資料物件

如上一節例子所示,當資料物件過於複雜時,如果採用Assert。assertEquals依次驗證每個JavaBean物件、驗證每一個屬性欄位,測試用例的程式碼量將會非常龐大。這裡,推薦使用序列化手段簡化資料物件的驗證,比如利用JSON。toJSONString方法把複雜的資料物件轉化為字串,然後再使用Assert。assertEquals方法進行驗證字串。但是,序列化值必須具備有序性、一致性和可讀性。

ListUserDOuserList = 。。。;String text = ResourceHelper。getResourceAsString(getClass(), “userList。json”);Assert。assertEquals(“使用者列表不一致”, text, JSON。toJSONString(userList));

通常使用JSON。toJSONString方法把Map物件轉化為字串,其中key-value的順序具有不確定性,無法用於驗證兩個物件是否一致。這裡,JSON提供序列化選項SerializerFeature。MapSortField(對映排序欄位),可以用於保證序列化後的key-value的有序性。

MapLong, MapString, ObjectuserMap = 。。。;String text = ResourceHelper。getResourceAsString(getClass(), “userMap。json”);Assert。assertEquals(“使用者對映不一致”, text, JSON。toJSONString(userMap, SerializerFeature。MapSortField));

9。4。6。驗證資料物件私有屬性欄位

有時候,單元測試用例需要對複雜物件的私有屬性欄位進行驗證。而PowerMockito提供的Whitebox。getInternalState方法,獲取輕鬆地獲取到私有屬性欄位值。

MapperScannerConfigurer configurer = myBatisConfiguration。buildMapperScannerConfigurer();Assert。assertEquals(“基礎包不一致”, “com。alibaba。example”, Whitebox。getInternalState(configurer, “basePackage”));

9。5。驗證異常物件內容

異常作為Java語言的重要特性,是Java語言健壯性的重要體現。捕獲並驗證異常資料內容,也是測試用例的一種。

9。5。1。透過@Test註解驗證異常物件

JUnit的註解@Test提供了一個expected屬性,可以指定一個期望的異常型別,用來捕獲並驗證異常。但是,這種方式只能驗證異常型別,並不能驗證異常原因和訊息。

@Test(expected = ExampleException。class)public void testGetUser() { // 模擬依賴方法 Mockito。doReturn(null)。when(userDAO)。get(userId); // 呼叫被測方法 userService。getUser(userId);}

9。5。2。透過@Rule註解驗證異常物件

如果想要驗證異常原因和訊息,就需求採用@Rule註解定義ExpectedException物件,然後在測試方法的前面宣告要捕獲的異常型別、原因和訊息。

@Ruleprivate ExpectedException exception = ExpectedException。none();@Testpublic void testGetUser() { // 模擬依賴方法 Long userId = 123L; Mockito。doReturn(null)。when(userDAO)。get(userId); // 呼叫被測方法 exception。expect(ExampleException。class); exception。expectMessage(String。format(“使用者(%s)不存在”, userId)); userService。getUser(userId);}

9。5。3。透過Assert。assertThrows驗證異常物件

在最新版的JUnit中,提供了一個更為簡潔的異常驗證方式——Assert。assertThrows方法。

@Testpublic void testGetUser() { // 模擬依賴方法 Long userId = 123L; Mockito。doReturn(null)。when(userDAO)。get(userId); // 呼叫被測方法 ExampleException exception = Assert。assertThrows(“異常型別不一致”, ExampleException。class, () -userService。getUser(userId)); Assert。assertEquals(“異常訊息不一致”, “處理異常”, exception。getMessage());}

10。驗證依賴物件

10。1。驗證模擬物件沒有任何方法呼叫

Mockito提供了verifyNoInteractions方法,可以驗證模擬物件在被測試方法中沒有任何呼叫。

Mockito。verifyNoInteractions(idGenerator, userDAO);

10。2。驗證模擬物件沒有更多方法呼叫

Mockito提供了verifyNoMoreInteractions方法,在驗證模擬物件所有方法呼叫後使用,可以驗證模擬物件所有方法呼叫是否都得到驗證。如果模擬物件存在任何未驗證的方法呼叫,就會丟擲NoInteractionsWanted異常。

Mockito。verifyNoMoreInteractions(idGenerator, userDAO);

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

10。3。清除模擬物件所有方法呼叫標記

在編寫單元測試用例時,為了減少單元測試用例數和程式碼量,可以把多組引數定義在同一個單元測試用例中,然後用for迴圈依次執行每一組引數的被測方法呼叫。為了避免上一次測試的方法呼叫影響下一次測試的方法呼叫驗證,最好使用Mockito提供clearInvocations方法清除上一次的方法呼叫。

// 清除所有物件呼叫Mockito。clearInvocations();// 清除指定物件呼叫Mockito。clearInvocations(idGenerator, userDAO);

11。典型案例分析

這裡,只收集了幾個經典案例,解決了特定環境下的特定問題。

11。1。測試框架特性導致問題

在編寫單元測試用例時,或多或少會遇到一些問題,大多數是由於對測試框架特性不熟悉導致,比如:

Mockito不支援對靜態方法、構造方法、final方法、私有方法的模擬;

Mockito的any相關的引數匹配方法並不支援可空引數和空引數;

採用Mockito的引數匹配方法時,其它引數不能直接用常量或變數,必須使用Mockito的eq方法;

使用when-then語句模擬Spy物件方法會先執行真實方法,應該使用do-when語句;

PowerMock對靜態方法、構造方法、final方法、私有方法的模擬需要把對應的類新增到@PrepareForTest註解中;

PowerMock模擬JDK的靜態方法、構造方法、final方法、私有方法時,需要把使用這些方法的類加入到@PrepareForTest註解中,從而導致單元測試覆蓋率不被統計;

PowerMock使用自定義的類載入器來載入類,可能導致系統類載入器認為有型別轉化問題;需要加上@PowerMockIgnore({“javax。crypto。*”})註解,來告訴PowerMock這個包不要用PowerMock的類載入器載入,需要採用系統類載入器來載入。

……

對於這些問題,可以根據提示資訊查閱相關資料解決,這裡就不再累述了。

11。2。捕獲引數值已變更問題

在編寫單元測試用例時,通常採用ArgumentCaptor進行引數捕獲,然後對引數物件值進行驗證。如果引數物件值沒有變更,這個步驟就沒有任何問題。但是,如果引數物件值在後續流程中發生變更,就會導致驗證引數值失敗。

原始程式碼:

publicTvoid readData(RecordReader recordReader, int batchSize, FunctionRecord, TdataParser, PredicateListTdataStorage) { try { // 依次讀取資料 Record record; boolean isContinue = true; ListTdataList = new ArrayList(batchSize); while (Objects。nonNull(record = recordReader。read())isContinue) { // 解析新增資料 T data = dataParser。apply(record); if (Objects。nonNull(data)) { dataList。add(data); } // 批次儲存資料 if (dataList。size() == batchSize) { isContinue = dataStorage。test(dataList); dataList。clear(); } } // 儲存剩餘資料 if (CollectionUtils。isNotEmpty(dataList)) { dataStorage。test(dataList); dataList。clear(); } } catch (IOException e) { String message = READ_DATA_EXCEPTION; log。warn(message, e); throw new ExampleException(message, e); }}

測試用例:

@Testpublic void testReadData() throws Exception { // 模擬依賴方法 // 模擬依賴方法: recordReader。read Record record0 = Mockito。mock(Record。class); Record record1 = Mockito。mock(Record。class); Record record2 = Mockito。mock(Record。class); TunnelRecordReader recordReader = Mockito。mock(TunnelRecordReader。class); Mockito。doReturn(record0, record1, record2, null)。when(recordReader)。read(); // 模擬依賴方法: dataParser。apply Object object0 = new Object(); Object object1 = new Object(); Object object2 = new Object(); FunctionRecord, ObjectdataParser = Mockito。mock(Function。class); Mockito。doReturn(object0)。when(dataParser)。apply(record0); Mockito。doReturn(object1)。when(dataParser)。apply(record1); Mockito。doReturn(object2)。when(dataParser)。apply(record2); // 模擬依賴方法: dataStorage。test PredicateListObjectdataStorage = Mockito。mock(Predicate。class); Mockito。doReturn(true)。when(dataStorage)。test(Mockito。anyList()); // 呼叫測試方法 odpsService。readData(recordReader, 2, dataParser, dataStorage); // 驗證依賴方法 // 模擬依賴方法: recordReader。read Mockito。verify(recordReader, Mockito。times(4))。read(); // 模擬依賴方法: dataParser。apply Mockito。verify(dataParser, Mockito。times(3))。apply(Mockito。any(Record。class)); // 驗證依賴方法: dataStorage。test ArgumentCaptorListObjectrecordListCaptor = ArgumentCaptor。forClass(List。class); Mockito。verify(dataStorage, Mockito。times(2))。test(recordListCaptor。capture()); Assert。assertEquals(“資料列表不一致”, Arrays。asList(Arrays。asList(object0, object1), Arrays。asList(object2)), recordListCaptor。getAllValues());}

問題現象:

執行單元測試用例失敗,丟擲以下異常資訊:

java。lang。AssertionError: 資料列表不一致 expected:[[java。lang。Object@1e3469df, java。lang。Object@79499fa], [java。lang。Object@48531d5]]but was:[[], []]

問題原因:

由於引數dataList在呼叫dataStorage。test方法後,都被主動呼叫dataList。clear方法進行清空。由於ArgumentCaptor捕獲的是物件引用,所以最後捕獲到了同一個空列表。

解決方案:

可以在模擬依賴方法dataStorage。test時,儲存傳入引數的當前值進行驗證。程式碼如下:

Java 程式設計技巧之單元測試用例編寫流程

11。3。模擬Lombok的log物件問題

Lombok的@Slf4j註解,廣泛地應用於Java專案中。在某些程式碼分支裡,可能只有log記錄日誌的操作,為了驗證這個分支邏輯被正確執行,需要在單元測試用例中對log記錄日誌的操作進行驗證。

原始方法:

@Slf4j@Servicepublic class ExampleService { public void recordLog(int code) { if (code == 1) { log。info(“執行分支1”); return; } if (code == 2) { log。info(“執行分支2”); return; } log。info(“執行預設分支”); } 。。。}

測試用例:

@RunWith(PowerMockRunner。class)public class ExampleServiceTest { @Mock private Logger log; @InjectMocks private ExampleService exampleService; @Test public void testRecordLog1() { exampleService。recordLog(1); Mockito。verify(log)。info(“執行分支1”); }}

問題現象:

執行單元測試用例失敗,丟擲以下異常資訊:

Wanted but not invoked:logger。info(“執行分支1”);

原因分析:

經過調式跟蹤,發現ExampleService中的log物件並沒有被注入。透過編譯發現,Lombok的@Slf4j註解在ExampleService類中生成了一個靜態常量log,而@InjectMocks註解並不支援靜態常量的注入。

解決方案:

採用作者實現的FieldHelper。setStaticFinalField方法,可以實現對靜態常量的注入模擬物件。

@RunWith(PowerMockRunner。class)public class ExampleServiceTest { @Mock private Logger log; @InjectMocks private ExampleService exampleService; @Before public void beforeTest() throws Exception { FieldHelper。setStaticFinalField(ExampleService。class, “log”, log); } @Test public void testRecordLog1() { exampleService。recordLog(1); Mockito。verify(log)。info(“執行分支1”); }}

11。4。相容Pandora等容器問題

阿里巴巴的很多中介軟體,都是基於Pandora容器的,在編寫單元測試用例時,可能會遇到一些坑。

原始方法:

@Slf4jpublic class MetaqMessageSender { @Autowired private MetaProducer metaProducer; public String sendMetaqMessage(String topicName, String tagName, String messageKey, String messageBody) { try { // 組裝訊息內容 Message message = new Message(); message。setTopic(topicName); message。setTags(tagName); message。setKeys(messageKey); message。setBody(messageBody。getBytes(StandardCharsets。UTF_8)); // 傳送訊息請求 SendResult sendResult = metaProducer。send(message); if (sendResult。getSendStatus() != SendStatus。SEND_OK) { String msg = String。format(“傳送標籤(%s)訊息(%s)狀態錯誤(%s)”, tagName, messageKey, sendResult。getSendStatus()); log。warn(msg); throw new ReconsException(msg); } log。info(String。format(“傳送標籤(%s)訊息(%s)狀態成功:%s”, tagName, messageKey, sendResult。getMsgId())); // 返回訊息標識 return sendResult。getMsgId(); } catch (MQClientException | RemotingException | MQBrokerException | InterruptedException e) { // 記錄訊息異常 Thread。currentThread()。interrupt(); String message = String。format(“傳送標籤(%s)訊息(%s)狀態異常:%s”, tagName, messageKey, e。getMessage()); log。warn(message, e); throw new ReconsException(message, e); } }}

測試用例:

@RunWith(PowerMockRunner。class)public class MetaqMessageSenderTest { @Mock private MetaProducer metaProducer; @InjectMocks private MetaqMessageSender metaqMessageSender; @Test public void testSendMetaqMessage() throws Exception { // 模擬依賴方法 SendResult sendResult = new SendResult(); sendResult。setMsgId(“msgId”); sendResult。setSendStatus(SendStatus。SEND_OK); Mockito。doReturn(sendResult)。when(metaProducer)。send(Mockito。any(Message。class)); // 呼叫測試方法 String topicName = “topicName”; String tagName = “tagName”; String messageKey = “messageKey”; String messageBody = “messageBody”; String messageId = metaqMessageSender。sendMetaqMessage(topicName, tagName, messageKey, messageBody); Assert。assertEquals(“messageId不一致”, sendResult。getMsgId(), messageId); // 驗證依賴方法 ArgumentCaptorMessagemessageCaptor = ArgumentCaptor。forClass(Message。class); Mockito。verify(metaProducer)。send(messageCaptor。capture()); Message message = messageCaptor。getValue(); Assert。assertEquals(“topicName不一致”, topicName, message。getTopic()); Assert。assertEquals(“tagName不一致”, tagName, message。getTags()); Assert。assertEquals(“messageKey不一致”, messageKey, message。getKeys()); Assert。assertEquals(“messageBody不一致”, messageBody, new String(message。getBody())); }}

問題現象:

執行單元測試用例失敗,丟擲以下異常資訊:

java。lang。RuntimeException: com。alibaba。rocketmq。client。producer。SendResult was loaded by org。powermock。core。classloader。javassist。JavassistMockClassLoader@5d43661b, it should be loaded by Pandora Container。 Can not load this fake sdk class。

原因分析:

基於Pandora容器的中介軟體,需要使用Pandora容器載入。在上面測試用例中,使用了PowerMock容器載入,從而導致丟擲類載入異常。

解決方案:

首先,把PowerMockRunner替換為PandoraBootRunner。其次,為了使@Mock、@InjectMocks等Mockito註解生效,需要呼叫MockitoAnnotations。initMocks(this)方法進行初始化。

@RunWith(PandoraBootRunner。class)public class MetaqMessageSenderTest { 。。。 @Before public void beforeTest() { MockitoAnnotations。initMocks(this); } 。。。}

12。消除型別轉換警告

在編寫測試用例時,特別是泛型型別轉換時,很容易產生型別轉換警告。常見型別轉換警告如下:

Java 程式設計技巧之單元測試用例編寫流程

作為一個有程式碼潔癖的輕微強迫症程式設計師,是絕對不容許這些型別轉換警告產生的。於是,總結了以下方法來解決這些型別轉換警告。

12。1。利用註解初始化

Mockito提供@Mock註解來模擬類例項,提供@Captor註解來初始化引數捕獲器。由於這些註解例項是透過測試框架進行初始化的,所以不會產生型別轉換警告。

問題程式碼:

Java 程式設計技巧之單元測試用例編寫流程

建議程式碼:

Java 程式設計技巧之單元測試用例編寫流程

12。2。利用臨時類或介面

我們無法獲取泛型類或介面的class例項,但是很容易獲取具體類的class例項。這個解決方案的思路是——先定義繼承泛型類的具體子類,然後mock、spy、forClass以及any出這個具體子類的例項,然後把具體子類例項轉換為父類泛型例項。

問題程式碼:

Java 程式設計技巧之單元測試用例編寫流程

建議程式碼:

Java 程式設計技巧之單元測試用例編寫流程

12。3。利用CastUtils。cast方法

SpringData包中提供一個CastUtils。cast方法,可以用於型別的強制轉換。這個解決方案的思路是——利用CastUtils。cast方法遮蔽型別轉換警告。

問題程式碼:

Java 程式設計技巧之單元測試用例編寫流程

建議程式碼:

Java 程式設計技巧之單元測試用例編寫流程

這個解決方案,不需要定義註解,也不需要定義臨時類或介面,能夠讓測試用例程式碼更為精簡,所以作者重點推薦。如果不願意引入SpringData包,也可以自己參考實現該方法,只是該方法會產生型別轉換警告。

注意:CastUtils。cast方法本質是——先轉換為Object型別,再強制轉換對應型別,本身不會對型別進行校驗。所以,CastUtils。cast方法好用,但是不要亂用,否則就是大坑(只有執行時才能發現問題)。

12。4。利用型別自動轉換

在Mockito中,提供形式如下的方法——泛型型別只跟返回值有關,而跟輸入引數無關。這樣的方法,可以根據呼叫方法的引數型別自動轉換,而無需手動強制型別轉換。如果手動強制型別轉換,反而會產生型別轉換警告。

Java 程式設計技巧之單元測試用例編寫流程

問題程式碼:

Java 程式設計技巧之單元測試用例編寫流程

建議程式碼:

Java 程式設計技巧之單元測試用例編寫流程

其實,SpringData的CastUtils。cast方法之所以這麼強悍,也是採用了型別自動轉化方法。

12。5。利用doReturn-when語句代替when-thenReturn語句

Mockito的when-thenReturn語句需要對返回型別強制校驗,而doReturn-when語句不會對返回型別強制校驗。利用這個特性,可以利用doReturn-when語句代替when-thenReturn語句解決型別轉換警告。

問題程式碼:

Java 程式設計技巧之單元測試用例編寫流程

建議程式碼:

Java 程式設計技巧之單元測試用例編寫流程

12。6。利用instanceof關鍵字

JDK提供的Method。invoke方法返回的是Object型別,轉化為具體型別時需要強制轉換,會產生型別轉換警告。而PowerMock提供的Whitebox。invokeMethod方法返回型別可以自動轉化,不會產生型別轉換警告。

問題程式碼:

Java 程式設計技巧之單元測試用例編寫流程

建議程式碼:

Java 程式設計技巧之單元測試用例編寫流程

12。7。利用instanceof關鍵字

在具體型別強制轉換時,建議利用instanceof關鍵字先判斷型別,否則會產生型別轉換警告。

JSONArray jsonArray = (JSONArray)object;。。。

建議程式碼:

if (object instanceof JSONArray) { JSONArray jsonArray = (JSONArray)object; 。。。}

12。8。利用Class。cast方法

在泛型型別強制轉換時,會產生型別轉換警告。可以採用泛型類的cast方法轉換,從而避免產生型別轉換警告。

問題程式碼:

Java 程式設計技巧之單元測試用例編寫流程

建議程式碼:

Java 程式設計技巧之單元測試用例編寫流程

12。9。避免不必要的型別轉換

有時候,沒有必要進行型別轉換,就儘量避免型別轉換。比如:把Object型別轉換為具體型別,但又把具體型別當Object型別使用,就沒有必要進行型別轉換。像這種情況,可以合併表示式或定義基類變數,從而避免不必要的型別轉化。

問題程式碼:

Java 程式設計技巧之單元測試用例編寫流程

建議程式碼:

Java 程式設計技巧之單元測試用例編寫流程

後記

登妙峰山記山高路遠車難騎,精疲力盡人易棄。多少妙峰登頂者,又練心境又練力!

騎行的人,一定要沉得住氣、要吃得了苦、要耐得住寂寞、要意志堅定不移、要體力夠猛夠持久……恰好,這也正是技術人所要具備的精神。只要技術人做到了這些,練就了好的“心境”和“體力”,才有可能登上技術的“妙峰山”。

作者 | 常意

相關文章