一种极简单的SpringBoot单元测试方法
本文主要提供了一种单元测试方法,力求0基础人员可以从本文中受到启发,可以搭建一套好用的单元测试环境,并能切实提高交付代码的质量。极简体现在除了POM依赖和单元测试类之外,其他什么都不需要引入,只需要一个本地能启动的springboot项目。
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>
/*** @author jiangbo8* @since 2024/4/24 9:52*/@Transactional@SpringBootTest@RunWith(SpringRunner.class)public class SalesAmountPlanControllerAppTest {@Resourceprivate SalesAmountPlanController salesAmountPlanController;@SpyBeanprivate ISaleAmountHourHistoryService saleAmountHourHistoryServiceImpl;@SpyBeanprivate ISaleAmountHourForecastService saleAmountHourForecastServiceImpl;@SpyBeanprivate ISaleAmountHourPlanService saleAmountHourPlanServiceImpl;@Testpublic 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);}@Testpublic void testGraph11() {// mock就走mockMockito.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快速生成单元测试类的框架
/*** Created by jiangbo8 on 2022/10/17 15:02*/public class CheckAndFillProcessorTest {@MockLogger log;@MockOrderRelService orderRelService;@MockVenderServiceSdk venderServiceSdk;@MockAfsServiceSdk afsServiceSdk;@MockPriceServiceSdk priceServiceSdk;@MockProductInfoSdk productInfoSdk;@MockOrderMidServiceSdk orderMidServiceSdk;@MockOrderQueueService orderQueueService;@MockSendpayMarkService sendpayMarkService;@MockTradeOrderService tradeOrderService;@InjectMocksCheckAndFillProcessor checkAndFillProcessor;@Beforepublic void setUp() {MockitoAnnotations.initMocks(this);}@Testpublic 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就可以了,但是如果某场景极为复杂,想要单独对某个复杂计算代码块进行专门的测试,那么可以使用第二种方法,第二种方法是很单纯的单元测试,聚焦专门代码块,但是如果普遍使用的话,单元测试代码编写量会很大,不建议单纯使用某一种,可以具体情况具体分析。
建议大家做单元测试不要单纯的追求行覆盖率,还是要本着提高质量的心态去做单元测试。
- END -