一种极简单的SpringBoot单元测试方法| 京东零售技术团队

前言

本文主要提供了一种单元测试方法,力求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就可以了,但是如果某场景极为复杂,想要单独对某个复杂计算代码块进行专门的测试,那么可以使用第二种方法,第二种方法是很单纯的单元测试,聚焦专门代码块,但是如果普遍使用的话,单元测试代码编写量会很大,不建议单纯使用某一种,可以具体情况具体分析。

建议大家做单元测试不要单纯的追求行覆盖率,还是要本着提高质量的心态去做单元测试。

作者:京东零售 姜波

来源:京东云开发者社区

点赞
收藏

评论区

加载中...

相关推荐

java单元测试工具

前言随着DevOp的不断流行,自动化测试慢慢成为Java开发者的关注点。因此,本文将分享10款优秀的单元测试框架和库,它们可以帮助Java开发人员在其Java项目上编写单元测试和集成测试。1\.JUnit我绝对JUnit不需要太多的介绍了。即使您是Java初学者,也可能听说过它。它可以帮助您为Java代码编写单元测试。几

高效易用的C++单元测试框架:轻松构建高质量代码

单元测试是构建稳定、高质量的程序、服务或系统的必不可少的一环。通过单元测试,我们可以在开发过程中及时发现和修复代码中的问题,提高代码的质量和可维护性。同时,单元测试也可以帮助我们更好地理解代码的功能和实现细节,从而更好地进行代码重构和优化。

单元测试利器——手把手教你使用Mockito

作者:京东零售秦浩然从你成为开发人员的那一天起,写单元测试终究是你逃不开的宿命!那开发人员为什么不喜欢写单元测试呢?究其原因,无外乎是依赖。依赖其他的服务、依赖运行的环境、等等,各种依赖都成为了我们写单元测试的绊脚石。那现在有个单元测试利器可以帮我们解决依

云原生引擎单元测试实践

快速迭代的开发工作中如何提高代码质量一直是团队痛点,特别是没有测试支持的开发团队。合理的使用单元测试,并关注单元测试通过率、代码覆盖率可以有效提高代码质量。今天就来讲讲云原生引擎单元测试实践。

OpenStack基础知识

针对以前学的内容的一个简单整理1、单元测试工具介绍unittest:是Python的标准库,提供了最基本的单元测试功能,包括单元测试运行器(简称runner)和单元测试框架。项目的单元测试代码的测试类可以继承unittest.TestCase类,那么这个类就能够被runner发现并且执行。同时,unittest.TestCase

Mock工具之Mockito实战

在实际项目中写单元测试的过程中我们会发现需要测试的类有很多依赖,这些依赖项又会有依赖,导致在单元测试代码里几乎无法完成构建,尤其是当依赖项尚未构建完成时会导致单元测试无法进行。为了解决这类问题我们引入了Mock的概念,简单的说就是模拟这些需要构建的类或者资源,提供给需要测试的对象使用。业内的Mock工具有很多,也已经很成熟了,这里我们将直接使用最流行的Moc