背景
作為開發人員,在代碼交付QA前,為了保證交付質量和代碼正確性,一般對代碼進行單元測驗,單測一般由Mock和斷言兩部分組成,大部分情況下,我們會針對要測驗類的成員物件方法呼叫的回傳值進行Mock,然后通過斷言去判斷方法的邏輯是否符合預期,但是一些情況下,我們會發現一些代碼的回傳值是Void這樣的話我們便無法根據回傳值進行斷言操作,此外還有一些方法可能含有中途回傳的Case即在某些情況下直接回傳了,不執行接下來的邏輯,這樣的也無法直接通過斷言工具去判斷方法邏輯的準確性,這時候,我們就需要用到Mock框架的一些功能來進行校驗,本文以Mockito為例,來展示如何對這些場景進行單元測驗,
原理
一個方法有三個組成部分,入參、邏輯以及回傳值,單測便可由這三個部分入手,而入參是決定執行邏輯的,所以一般情況下我們可以針對邏輯和單測進行單元測驗,大部分情況下,邏輯由Mock工具掌管,而回傳值則依靠斷言工具管理,在沒有回傳值的情況下,通過斷言驗證的方法走不通,那么就可以從邏輯的角度入手通過Mock工具來驗證邏輯是否執行正確,由于在進行單元測驗的情況下,我們一般會對底層呼叫用Mock物件屏蔽,而通過Mock框架比如Mockito進行Mock時,在方法運行后,Mock物件的互動情況是有記錄的,所以我們可以通過這些Mock物件的呼叫資訊來判斷代碼邏輯的正確性,
對于Mockito我們可以從Verify的底層實作方法org.mockito.internal.MockitoCore#verify入手,Mockito提供的verifyNoInteractions等方法的基礎實作皆是該方法,具體代碼如下:
public <T> T verify(T mock, VerificationMode mode) {
if (mock == null) {
throw nullPassedToVerify();
}
MockingDetails mockingDetails = mockingDetails(mock);
if (!mockingDetails.isMock()) {
throw notAMockPassedToVerify(mock.getClass());
}
assertNotStubOnlyMock(mock);
MockHandler handler = mockingDetails.getMockHandler();
mock = (T) VerificationStartedNotifier.notifyVerificationStarted(
handler.getMockSettings().getVerificationStartedListeners(), mockingDetails);
MockingProgress mockingProgress = mockingProgress();
VerificationMode actualMode = mockingProgress.maybeVerifyLazily(mode);
mockingProgress.verificationStarted(new MockAwareVerificationMode(mock, actualMode, mockingProgress.verificationListeners()));
return mock;
}
從以上定義我們可以看出verify介面是對Mock物件的VerificationMode校驗模式進行校驗,而VerificationMode是一個介面其方法如下:
public interface VerificationMode {
/**
* 這個是主要實作方法,verifycationData包含了Mock物件的呼叫資訊,可根據呼叫資訊來實作自己的校驗方法
*/
void verify(VerificationData data);
VerificationMode description(String description);
}
Mockito自帶了一些該介面的實作,我們可以通過VerificationModeFactory這個類找到他們,大部分是關于呼叫資訊的,如呼叫次數等,參考這些介面的實作,自己也能實作一些校驗模式,
實踐
比如針對如下這段代碼一個常見的冪等處理方法,業務背景不仔細介紹了,大概流程是對于資料的uuid已經消費過的的情況跳過不執行邏輯,沒有消費過的則要繼續執行保存邏輯,這段方法有兩個顯著特點,一是回傳值為void,二是存在中途跳出邏輯的情況,這種情況下,針對這段代碼,我們需要寫兩個單測case來確保邏輯是正確的,即
- uuid不存在,需要確保對資料進行保存操作,且保存的值符合預期,
- uuid已經存,介面冪等不做保存處理,僅列印日志,
@Override
@Transactional(rollbackFor = Throwable.class)
public void saveOrder(List<Order> orders) {
Map<String, List<Order>> orderMap = orders.stream().collect(Collectors.groupingBy(Order::getUuid));
for (String uuid : orderMap.keySet()) {
if (exists(uuid, orderMap.get(uuid))) {
log.error("接收單據uuid重復,{}", uuid);
// 重復跳過,不拋例外
continue;
}
orderDao.insertList(convert(orderMap.get(uuid)));
List<OrderDetail> orderDetails = orderMap.get(uuid)
.stream()
.map(OrderDetail::getOrderDetails)
.flatMap(Collection::stream)
.collect(Collectors.toList());
orderDetailDao.insertList(convertDetails(orderDetails));
}
}
對于這種void的回傳值,并且也沒有拋例外的出現,我們無法對回傳值進行斷言,而且關鍵是由于流程有跳過的可能,使用斷言框架是無法驗證這種流程的,但由于我們這個邏輯中的物件是有Mock物件的即OrderDao和OrderDetailDao,所以我們可以利用Mockito的verify校驗功能對單測的Mock物件的互動情況做一個斷言處理,而這個就依賴于Mockito的verify功能,
-
下面代碼表示是針對case1即不存在原uuid,這樣我們需要確保有互動并且互動資料和預期一致,這里使用verify+ArgumentCaptors的對Mock物件的入參進行抓取,然后使用再使用斷言工具判斷入參是否符合預期,其實個人認為用verify+ArgumentMathers的方法更正確,因為這里是對邏輯校驗單純使用Mock框架將更明顯驗證這一點,但為了更好看還是使用了Mock+斷言的方式驗證方法,
@Test @DisplayName("保存資料不存在原uuid") void testSaveOrderNotExist() { Order order = new Order(); order.setOrderNo("son1"); order.setUuid("son1"); order.setOrderDetails(Collections.singletonList(new OrderDetail())); OrderPo orderPo = new OrderPo(); orderPo.setOrderNo("son1"); orderPo.setUuid("son1"); when(orderDao.insertList(Collections.singletonList(orderPo))).thenReturn(1); when(orderDetailDao.insertList(anyList())).thenReturn(1); Uuid bizUuid = new Uuid(); bizUuid.setBusinessNo("son1"); bizUuid.setUuid("son1"); bizUuid.setOperateType(TaskAssignConstant.INIT_ORDER_OPERATE_TYPE); // 這里的mock回傳值影響exist方法的回傳值1代表未存在 when(taskAssignUuidDao.insertIgnore(bizUuid)).thenReturn(1); orderRepositoryImpl.saveOrder(Collections.singletonList(order)); /* * 這里使用Mockito的verify方法通過ArgumentCaptor對mock物件orderDao的入參進行抓取, * 然后通過斷言判斷該Mock物件的互動引數是否符合預期,使用ArgumentCaptor可以抓取引數通過斷言判斷, * 也可直接對入參進行構造,將使用物件的equals方法進行判斷,也可使用ArgumentMathers構造一個匹配引數方法驗證, */ ArgumentCaptor<List<OrderPo>> argumentCaptor = ArgumentCaptor.forClass(List.class); verify(orderDao).insertList(argumentCaptor.capture()); OrderPo orderPo1 = argumentCaptor.getValue().get(0); Assertions.assertEquals("son1", orderPo1.getOrderNo()); Assertions.assertEquals("son1", orderPo1.getUuid()); } -
下圖針對case2,即存在原uuid,由于原代碼存在uuid直接continue相當于跳過了下面的流程,所以需要使用verfiy校驗mock的物件在這個case執行時沒有互動,
@Test @DisplayName("保存資料存在原uuid") void testSaveorderExist() { order order = new order(); order.setorderNo("son1"); order.setUuid("son1"); order.setWarehouseNo("6_6_618"); orderPo orderPo = new orderPo(); orderPo.setorderNo("son1"); orderPo.setUuid("son1"); orderPo.setWarehouseNo("6_6_618"); when(orderDao.insertList(Collections.singletonList(orderPo))).thenReturn(1); when(orderDetailDao.insertList(any())).thenReturn(0); Uuid bizUuid = new Uuid(); bizUuid.setWarehouseNo("6_6_618"); bizUuid.setBusinessNo("son1"); bizUuid.setUuid("son1"); bizUuid.setOperateType(TaskAssignConstant.INIT_ORDER_OPERATE_TYPE); when(taskAssignUuidDao.insertIgnore(bizUuid)).thenReturn(0); orderRepositoryImpl.saveorder(Collections.singletonList(order)); // 使用verifyNoInteractions 校驗mock物件在uuid已存在的情況下應該沒有互動 verifyNoInteractions(orderDao); verifyNoInteractions(orderDetailDao); }
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/311944.html
標籤:其他
上一篇:【HTTP】HTTP Body
