前言
本文主要提供了一種單元測試方法,力求0基礎人員可以從本文中受到啓發,可以搭建一套好用的單元測試環境,並能切實的提高交付代碼的質量。極簡體現在除了POM依賴和單元測試類之外,其他什麼都不需要引入,只需要一個本地能啓動的springboot項目。
目錄
1.POM依賴
2.單元測試類示例及註解釋義
3.單元測試經驗總結
一、POM依賴
Springboot版本: 2.6.6
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>3.12.4</version>
</dependency>
二、單元測試類示例
主要有兩種
第一種,偏集成測試
需要啓動項目,需要連接數據庫、RPC註冊中心等
主要註解:@SpringBootTest + @RunWith(SpringRunner.class) + @Transactional + @Resource + @SpyBean + @Test
•@SpringBootTest + @RunWith(SpringRunner.class) 啓動了一套springboot的測試環境;
•@Transactional 對於一些修改數據庫的操作,會執行回滾,能測試執行sql,但是又不會真正的修改測試庫的數據;
•@Resource 主要引入被測試的類
•@SpyBean springboot環境下mock依賴的bean,可以搭配Mockito.doAnswer(…).when(xxServiceImpl).xxMethod(any())mock特定方法的返回值;
•@Test 標識一個測試方法
TIP:對於打樁有這幾個註解@Mock @Spy @MockBean @SpyBean,每一個都有其對應的搭配,簡單說@Mock和@Spy要搭配@InjectMocks去使用,@MockBean和@SpyBean搭配@SpringBootTest + @RunWith(SpringRunner.class)使用,@InjectMocks不用啓動應用,它啓動了一個完全隔離的測試環境,無法使用spring提供的所有bean,所有的依賴都需要被mock
上代碼:
/**
* @author jiangbo8
* @since 2024/4/24 9:52
*/
@Transactional
@SpringBootTest
@RunWith(SpringRunner.class)
public class SalesAmountPlanControllerAppTest {
@Resource
private SalesAmountPlanController salesAmountPlanController;
@SpyBean
private ISaleAmountHourHistoryService saleAmountHourHistoryServiceImpl;
@SpyBean
private ISaleAmountHourForecastService saleAmountHourForecastServiceImpl;
@SpyBean
private ISaleAmountHourPlanService saleAmountHourPlanServiceImpl;
@Test
public void testGraph1() {
// 不寫mock就走實際調用
SalesAmountDTO dto = new SalesAmountDTO();
dto.setDeptId1List(Lists.newArrayList(35));
dto.setDeptId2List(Lists.newArrayList(235));
dto.setDeptId3List(Lists.newArrayList(100));
dto.setYoyType(YoyTypeEnum.SOLAR.getCode());
dto.setShowWeek(true);
dto.setStartYm("2024-01");
dto.setEndYm("2024-10");
dto.setTimeDim(GraphTimeDimensionEnum.MONTH.getCode());
dto.setDataType(SalesAmountDataTypeEnum.AMOUNT.getCode());
Result<ChartData> result = salesAmountPlanController.graph(dto);
System.out.println(JSON.toJSONString(result));
Assert.assertNotNull(result);
}
@Test
public void testGraph11() {
// mock就走mock
Mockito.doAnswer(this::mockSaleAmountHourHistoryListQuery).when(saleAmountHourHistoryServiceImpl).listBySaleAmountQueryBo(any());
Mockito.doAnswer(this::mockSaleAmountHourPlansListQuery).when(saleAmountHourPlanServiceImpl).listBySaleAmountQueryBo(any());
Mockito.doAnswer(this::mockSaleAmountHourForecastListQuery).when(saleAmountHourForecastServiceImpl).listBySaleAmountQueryBo(any());
SalesAmountDTO dto = new SalesAmountDTO();
dto.setDeptId1List(Lists.newArrayList(111));
dto.setDeptId2List(Lists.newArrayList(222));
dto.setDeptId3List(Lists.newArrayList(333));
dto.setYoyType(YoyTypeEnum.SOLAR.getCode());
dto.setShowWeek(true);
dto.setStartYm("2024-01");
dto.setEndYm("2024-10");
dto.setTimeDim(GraphTimeDimensionEnum.MONTH.getCode());
dto.setDataType(SalesAmountDataTypeEnum.AMOUNT.getCode());
Result<ChartData> result = salesAmountPlanController.graph(dto);
System.out.println(JSON.toJSONString(result));
Assert.assertNotNull(result);
}
private List<SaleAmountHourHistory> mockSaleAmountHourHistoryListQuery(org.mockito.invocation.InvocationOnMock s) {
SaleAmountQueryBo queryBo = s.getArgument(0);
if (queryBo.getGroupBy().contains("ymd")) {
List<SaleAmountHourHistory> historyList = Lists.newArrayList();
List<String> ymdList = DateUtil.rangeWithDay(DateUtil.parseFirstDayLocalDate(queryBo.getStartYm()), DateUtil.parseLastDayLocalDate(queryBo.getStartYm()));
for (String ymd : ymdList) {
SaleAmountHourHistory history = new SaleAmountHourHistory();
history.setYear(Integer.parseInt(queryBo.getStartYm().split("-")[0]));
history.setMonth(Integer.parseInt(queryBo.getStartYm().split("-")[1]));
history.setYm(queryBo.getStartYm());
history.setYmd(DateUtil.parseLocalDateByYmd(ymd));
history.setAmount(new BigDecimal("1000"));
history.setAmountSp(new BigDecimal("2000"));
history.setAmountLunarSp(new BigDecimal("3000"));
history.setSales(new BigDecimal("100"));
history.setSalesSp(new BigDecimal("200"));
history.setSalesLunarSp(new BigDecimal("300"));
history.setCostPrice(new BigDecimal("100"));
history.setCostPriceSp(new BigDecimal("100"));
history.setCostPriceLunarSp(new BigDecimal("100"));
historyList.add(history);
}
return historyList;
}
List<String> ymList = DateUtil.rangeWithMonth(DateUtil.parseFirstDayLocalDate(queryBo.getStartYm()), DateUtil.parseLastDayLocalDate(queryBo.getEndYm()));
List<SaleAmountHourHistory> historyList = Lists.newArrayList();
for (String ym : ymList) {
SaleAmountHourHistory history = new SaleAmountHourHistory();
history.setYear(Integer.parseInt(ym.split("-")[0]));
history.setMonth(Integer.parseInt(ym.split("-")[1]));
history.setYm(ym);
history.setAmount(new BigDecimal("10000"));
history.setAmountSp(new BigDecimal("20000"));
history.setAmountLunarSp(new BigDecimal("30000"));
history.setSales(new BigDecimal("1000"));
history.setSalesSp(new BigDecimal("2000"));
history.setSalesLunarSp(new BigDecimal("3000"));
history.setCostPrice(new BigDecimal("100"));
history.setCostPriceSp(new BigDecimal("100"));
history.setCostPriceLunarSp(new BigDecimal("100"));
historyList.add(history);
}
return historyList;
}
}
第二種,單元測試
不需要啓動項目,也不會連接數據庫、RPC註冊中心等,但是相應的所有數據都需要打樁mock
這種方法可以使用testMe快速生成單元測試類的框架,具體方法見: 基於testMe快速生成單元測試類(框架)
主要註解:@InjectMocks + @Mock + @Test
•@InjectMocks標識了一個需要被測試的類,這個類中依賴的bean都需要被@Mock,並mock返回值,不然就會空指針
•@Mock mock依賴,具體mock數據還要搭配when(xxService.xxMethod(any())).thenReturn(new Object()); mock返回值
•@Test 標識一個測試方法
上代碼:
/**
* Created by jiangbo8 on 2022/10/17 15:02
*/
public class CheckAndFillProcessorTest {
@Mock
Logger log;
@Mock
OrderRelService orderRelService;
@Mock
VenderServiceSdk venderServiceSdk;
@Mock
AfsServiceSdk afsServiceSdk;
@Mock
PriceServiceSdk priceServiceSdk;
@Mock
ProductInfoSdk productInfoSdk;
@Mock
OrderMidServiceSdk orderMidServiceSdk;
@Mock
OrderQueueService orderQueueService;
@Mock
SendpayMarkService sendpayMarkService;
@Mock
TradeOrderService tradeOrderService;
@InjectMocks
CheckAndFillProcessor checkAndFillProcessor;
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
}
@Test
public void testProcess2() throws Exception {
OrderRel orderRel = new OrderRel();
//orderRel.setJdOrderId(2222222L);
orderRel.setSopOrderId(1111111L);
orderRel.setVenderId("123");
when(orderRelService.queryOrderBySopOrderId(anyLong())).thenReturn(orderRel);
OrderDetailRel orderDetailRel = new OrderDetailRel();
orderDetailRel.setJdSkuId(1L);
when(orderRelService.queryDetailList(any())).thenReturn(Collections.singletonList(orderDetailRel));
Vender vender = new Vender();
vender.setVenderId("123");
vender.setOrgId(1);
when(venderServiceSdk.queryVenderByVenderId(anyString())).thenReturn(vender);
when(afsServiceSdk.queryAfsTypeByJdSkuAndVender(anyLong(), anyString())).thenReturn(0);
when(priceServiceSdk.getJdToVenderPriceByPriorityAndSaleTime(anyString(), anyString(), any())).thenReturn(new BigDecimal("1"));
when(productInfoSdk.getProductInfo(any())).thenReturn(new HashMap<Long, Map<String, String>>() {{
put(1L, new HashMap<String, String>() {{
put("String", "String");
}});
}});
when(orderQueueService.updateQueueBySopOrderId(any())).thenReturn(true);
Order sopOrder = new Order();
sopOrder.setYn(1);
when(orderMidServiceSdk.getOrderByIdFromMiddleWare(anyLong())).thenReturn(sopOrder);
when(sendpayMarkService.isFreshOrder(anyLong(), anyString())).thenReturn(true);
doNothing().when(tradeOrderService).fillOrderProduceTypeInfo(any(), anyInt(), any());
doNothing().when(tradeOrderService).fillOrderFlowFlagInfo(any(), any(), anyInt(), any());
Field field = ResourceContainer.class.getDeclaredField("allInPlateConfig");
field.setAccessible(true);
field.set("allInPlateConfig", new AllInPlateConfig());
OrderQueue orderQueue = new OrderQueue();
orderQueue.setSopOrderId(1111111L);
DispatchResult result = checkAndFillProcessor.process(orderQueue);
Assert.assertNotNull(result);
}
}
三、單元測試經驗總結
在工作中總結了一些單元測試的使用場景:
1.重構,如果我們拿到了一個代碼,我們要去重構這個代碼,如果這個代碼本身的單元測試比較完善,那麼我們重構完之後可以執行一下現有的單元測試,以保證重構前後代碼在各個場景的邏輯保證最終一致,但是如果單元測試不完善甚至沒有,那我建議大家可以基於AI去生成這個代碼的單元測試,然後進行重構,再用生成的單元測試去把控質量,這裏推薦Diffblue去生成,有興趣的可以去了解一下。
2.新功能,新功能建議使用上面推薦的兩種方法去做單測,第一種方法因爲偏集成測試,單元測試代碼編寫的壓力比較小,可以以黑盒測試的視角去覆蓋測試case就可以了,但是如果某場景極爲複雜,想要單獨對某個複雜計算代碼塊進行專門的測試,那麼可以使用第二種方法,第二種方法是很單純的單元測試,聚焦專門代碼塊,但是如果普遍使用的話,單元測試代碼編寫量會很大,不建議單純使用某一種,可以具體情況具體分析。
建議大家做單元測試不要單純的追求行覆蓋率,還是要本着提高質量的心態去做單元測試。
作者:京東零售 姜波
來源:京東雲開發者社區