开发者社区 > 博文 > 一种极简单的SpringBoot单元测试方法
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

一种极简单的SpringBoot单元测试方法

  • 18****
  • 2024-05-08
  • IP归属:北京
  • 100浏览

    前言

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

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