前言
本文主要提供了一种单元测试方法,力求0基础人员可以从本文中受到启发,可以搭建一套好用的单元测试环境,并能切实的提高交付代码的质量。极简体现在除了POM依赖和单元测试类之外,其他什么都不需要引入,只需要一个本地能启动的springboot项目。
目录
1.POM依赖
2.单元测试类示例及注解释义
3.单元测试经验总结
一、POM依赖
Springboot版本: 2.6.6
1<dependency> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-test</artifactId> 4 <scope>test</scope> 5</dependency> 6<dependency> 7 <groupId>org.mockito</groupId> 8 <artifactId>mockito-core</artifactId> 9 <version>3.12.4</version> 10</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
上代码:
1/** 2 * @author jiangbo8 3 * @since 2024/4/24 9:52 4 */ 5@Transactional 6@SpringBootTest 7@RunWith(SpringRunner.class) 8public class SalesAmountPlanControllerAppTest { 9 @Resource 10 private SalesAmountPlanController salesAmountPlanController; 11 @SpyBean 12 private ISaleAmountHourHistoryService saleAmountHourHistoryServiceImpl; 13 @SpyBean 14 private ISaleAmountHourForecastService saleAmountHourForecastServiceImpl; 15 @SpyBean 16 private ISaleAmountHourPlanService saleAmountHourPlanServiceImpl; 17 18 @Test 19 public void testGraph1() { 20 // 不写mock就走实际调用 21 22 SalesAmountDTO dto = new SalesAmountDTO(); 23 dto.setDeptId1List(Lists.newArrayList(35)); 24 dto.setDeptId2List(Lists.newArrayList(235)); 25 dto.setDeptId3List(Lists.newArrayList(100)); 26 dto.setYoyType(YoyTypeEnum.SOLAR.getCode()); 27 dto.setShowWeek(true); 28 dto.setStartYm("2024-01"); 29 dto.setEndYm("2024-10"); 30 dto.setTimeDim(GraphTimeDimensionEnum.MONTH.getCode()); 31 dto.setDataType(SalesAmountDataTypeEnum.AMOUNT.getCode()); 32 Result<ChartData> result = salesAmountPlanController.graph(dto); 33 System.out.println(JSON.toJSONString(result)); 34 Assert.assertNotNull(result); 35 } 36 37 @Test 38 public void testGraph11() { 39 // mock就走mock 40 Mockito.doAnswer(this::mockSaleAmountHourHistoryListQuery).when(saleAmountHourHistoryServiceImpl).listBySaleAmountQueryBo(any()); 41 Mockito.doAnswer(this::mockSaleAmountHourPlansListQuery).when(saleAmountHourPlanServiceImpl).listBySaleAmountQueryBo(any()); 42 Mockito.doAnswer(this::mockSaleAmountHourForecastListQuery).when(saleAmountHourForecastServiceImpl).listBySaleAmountQueryBo(any()); 43 44 SalesAmountDTO dto = new SalesAmountDTO(); 45 dto.setDeptId1List(Lists.newArrayList(111)); 46 dto.setDeptId2List(Lists.newArrayList(222)); 47 dto.setDeptId3List(Lists.newArrayList(333)); 48 dto.setYoyType(YoyTypeEnum.SOLAR.getCode()); 49 dto.setShowWeek(true); 50 dto.setStartYm("2024-01"); 51 dto.setEndYm("2024-10"); 52 dto.setTimeDim(GraphTimeDimensionEnum.MONTH.getCode()); 53 dto.setDataType(SalesAmountDataTypeEnum.AMOUNT.getCode()); 54 Result<ChartData> result = salesAmountPlanController.graph(dto); 55 System.out.println(JSON.toJSONString(result)); 56 Assert.assertNotNull(result); 57 } 58 59 private List<SaleAmountHourHistory> mockSaleAmountHourHistoryListQuery(org.mockito.invocation.InvocationOnMock s) { 60 SaleAmountQueryBo queryBo = s.getArgument(0); 61 if (queryBo.getGroupBy().contains("ymd")) { 62 List<SaleAmountHourHistory> historyList = Lists.newArrayList(); 63 List<String> ymdList = DateUtil.rangeWithDay(DateUtil.parseFirstDayLocalDate(queryBo.getStartYm()), DateUtil.parseLastDayLocalDate(queryBo.getStartYm())); 64 for (String ymd : ymdList) { 65 SaleAmountHourHistory history = new SaleAmountHourHistory(); 66 history.setYear(Integer.parseInt(queryBo.getStartYm().split("-")[0])); 67 history.setMonth(Integer.parseInt(queryBo.getStartYm().split("-")[1])); 68 history.setYm(queryBo.getStartYm()); 69 history.setYmd(DateUtil.parseLocalDateByYmd(ymd)); 70 71 history.setAmount(new BigDecimal("1000")); 72 history.setAmountSp(new BigDecimal("2000")); 73 history.setAmountLunarSp(new BigDecimal("3000")); 74 75 history.setSales(new BigDecimal("100")); 76 history.setSalesSp(new BigDecimal("200")); 77 history.setSalesLunarSp(new BigDecimal("300")); 78 79 history.setCostPrice(new BigDecimal("100")); 80 history.setCostPriceSp(new BigDecimal("100")); 81 history.setCostPriceLunarSp(new BigDecimal("100")); 82 historyList.add(history); 83 } 84 85 return historyList; 86 } 87 88 List<String> ymList = DateUtil.rangeWithMonth(DateUtil.parseFirstDayLocalDate(queryBo.getStartYm()), DateUtil.parseLastDayLocalDate(queryBo.getEndYm())); 89 List<SaleAmountHourHistory> historyList = Lists.newArrayList(); 90 for (String ym : ymList) { 91 SaleAmountHourHistory history = new SaleAmountHourHistory(); 92 history.setYear(Integer.parseInt(ym.split("-")[0])); 93 history.setMonth(Integer.parseInt(ym.split("-")[1])); 94 history.setYm(ym); 95 96 history.setAmount(new BigDecimal("10000")); 97 history.setAmountSp(new BigDecimal("20000")); 98 history.setAmountLunarSp(new BigDecimal("30000")); 99 100 history.setSales(new BigDecimal("1000")); 101 history.setSalesSp(new BigDecimal("2000")); 102 history.setSalesLunarSp(new BigDecimal("3000")); 103 104 history.setCostPrice(new BigDecimal("100")); 105 history.setCostPriceSp(new BigDecimal("100")); 106 history.setCostPriceLunarSp(new BigDecimal("100")); 107 historyList.add(history); 108 } 109 110 return historyList; 111 } 112}
第二种,单元测试
不需要启动项目,也不会连接数据库、RPC注册中心等,但是相应的所有数据都需要打桩mock
这种方法可以使用testMe快速生成单元测试类的框架,具体方法见: 基于testMe快速生成单元测试类(框架)
主要注解:@InjectMocks + @Mock + @Test
•@InjectMocks标识了一个需要被测试的类,这个类中依赖的bean都需要被@Mock,并mock返回值,不然就会空指针
•@Mock mock依赖,具体mock数据还要搭配when(xxService.xxMethod(any())).thenReturn(new Object()); mock返回值
•@Test 标识一个测试方法
上代码:
1/** 2 * Created by jiangbo8 on 2022/10/17 15:02 3 */ 4public class CheckAndFillProcessorTest { 5 @Mock 6 Logger log; 7 @Mock 8 OrderRelService orderRelService; 9 @Mock 10 VenderServiceSdk venderServiceSdk; 11 @Mock 12 AfsServiceSdk afsServiceSdk; 13 @Mock 14 PriceServiceSdk priceServiceSdk; 15 @Mock 16 ProductInfoSdk productInfoSdk; 17 @Mock 18 OrderMidServiceSdk orderMidServiceSdk; 19 @Mock 20 OrderQueueService orderQueueService; 21 @Mock 22 SendpayMarkService sendpayMarkService; 23 @Mock 24 TradeOrderService tradeOrderService; 25 26 @InjectMocks 27 CheckAndFillProcessor checkAndFillProcessor; 28 29 @Before 30 public void setUp() { 31 MockitoAnnotations.initMocks(this); 32 } 33 34 @Test 35 public void testProcess2() throws Exception { 36 37 OrderRel orderRel = new OrderRel(); 38 //orderRel.setJdOrderId(2222222L); 39 orderRel.setSopOrderId(1111111L); 40 orderRel.setVenderId("123"); 41 42 when(orderRelService.queryOrderBySopOrderId(anyLong())).thenReturn(orderRel); 43 44 OrderDetailRel orderDetailRel = new OrderDetailRel(); 45 orderDetailRel.setJdSkuId(1L); 46 when(orderRelService.queryDetailList(any())).thenReturn(Collections.singletonList(orderDetailRel)); 47 48 Vender vender = new Vender(); 49 vender.setVenderId("123"); 50 vender.setOrgId(1); 51 when(venderServiceSdk.queryVenderByVenderId(anyString())).thenReturn(vender); 52 when(afsServiceSdk.queryAfsTypeByJdSkuAndVender(anyLong(), anyString())).thenReturn(0); 53 when(priceServiceSdk.getJdToVenderPriceByPriorityAndSaleTime(anyString(), anyString(), any())).thenReturn(new BigDecimal("1")); 54 when(productInfoSdk.getProductInfo(any())).thenReturn(new HashMap<Long, Map<String, String>>() {{ 55 put(1L, new HashMap<String, String>() {{ 56 put("String", "String"); 57 }}); 58 }}); 59 60 when(orderQueueService.updateQueueBySopOrderId(any())).thenReturn(true); 61 62 Order sopOrder = new Order(); 63 sopOrder.setYn(1); 64 when(orderMidServiceSdk.getOrderByIdFromMiddleWare(anyLong())).thenReturn(sopOrder); 65 66 when(sendpayMarkService.isFreshOrder(anyLong(), anyString())).thenReturn(true); 67 68 doNothing().when(tradeOrderService).fillOrderProduceTypeInfo(any(), anyInt(), any()); 69 doNothing().when(tradeOrderService).fillOrderFlowFlagInfo(any(), any(), anyInt(), any()); 70 71 Field field = ResourceContainer.class.getDeclaredField("allInPlateConfig"); 72 field.setAccessible(true); 73 field.set("allInPlateConfig", new AllInPlateConfig()); 74 75 OrderQueue orderQueue = new OrderQueue(); 76 orderQueue.setSopOrderId(1111111L); 77 DispatchResult result = checkAndFillProcessor.process(orderQueue); 78 Assert.assertNotNull(result); 79 } 80}
三、单元测试经验总结
在工作中总结了一些单元测试的使用场景:
1.重构,如果我们拿到了一个代码,我们要去重构这个代码,如果这个代码本身的单元测试比较完善,那么我们重构完之后可以执行一下现有的单元测试,以保证重构前后代码在各个场景的逻辑保证最终一致,但是如果单元测试不完善甚至没有,那我建议大家可以基于AI去生成这个代码的单元测试,然后进行重构,再用生成的单元测试去把控质量,这里推荐Diffblue去生成,有兴趣的可以去了解一下。
2.新功能,新功能建议使用上面推荐的两种方法去做单测,第一种方法因为偏集成测试,单元测试代码编写的压力比较小,可以以黑盒测试的视角去覆盖测试case就可以了,但是如果某场景极为复杂,想要单独对某个复杂计算代码块进行专门的测试,那么可以使用第二种方法,第二种方法是很单纯的单元测试,聚焦专门代码块,但是如果普遍使用的话,单元测试代码编写量会很大,不建议单纯使用某一种,可以具体情况具体分析。
建议大家做单元测试不要单纯的追求行覆盖率,还是要本着提高质量的心态去做单元测试。
作者:京东零售 姜波
来源:京东云开发者社区
