51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

一种极简单的 Spring Boot 单元测试方法

本文主要介绍了一种单元测试方法,力求零基础人员可以从本文中受到启发,可以搭建一套好用的单元测试环境,并能切实提高交付代码的质量。极简体现在除了 POM 依赖和单元测试类之外,其他什么都不需要引入,只需要一个本地能启动的 Spring Boot 项目。

1、POM依赖 {#1pom依赖}

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>

2、单元测试类示例 {#2单元测试类示例}

主要有两种。

第一种,偏集成测试 {#第一种偏集成测试}

需要启动项目,需要连接数据库、RPC 注册中心等。

主要注解:@SpringBootTest + @RunWith(SpringRunner.class) + @Transactional + @Resource + @SpyBean + @Test

  • @SpringBootTest + @RunWith(SpringRunner.class) 启动了一套 Spring Boot 的测试环境;
  • @Transactional 对于一些修改数据库的操作,会执行回滚,能测试执行 sql,但是又不会真正的修改测试库的数据;
  • @Resource 主要引入被测试的类;
  • @SpyBean Spring Boot 环境下 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(&quot;2024-01&quot;);
    dto.setEndYm(&quot;2024-10&quot;);
    dto.setTimeDim(GraphTimeDimensionEnum.MONTH.getCode());
    dto.setDataType(SalesAmountDataTypeEnum.AMOUNT.getCode());
    Result&lt;ChartData&gt; 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(&quot;2024-01&quot;);
    dto.setEndYm(&quot;2024-10&quot;);
    dto.setTimeDim(GraphTimeDimensionEnum.MONTH.getCode());
    dto.setDataType(SalesAmountDataTypeEnum.AMOUNT.getCode());
    Result&lt;ChartData&gt; 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(&quot;1000&quot;));
            history.setAmountSp(new BigDecimal(&quot;2000&quot;));
            history.setAmountLunarSp(new BigDecimal(&quot;3000&quot;));

            history.setSales(new BigDecimal(&quot;100&quot;));
            history.setSalesSp(new BigDecimal(&quot;200&quot;));
            history.setSalesLunarSp(new BigDecimal(&quot;300&quot;));

            history.setCostPrice(new BigDecimal(&quot;100&quot;));
            history.setCostPriceSp(new BigDecimal(&quot;100&quot;));
            history.setCostPriceLunarSp(new BigDecimal(&quot;100&quot;));
            historyList.add(history);
        }

        return historyList;
    }

    List&lt;String&gt; ymList = DateUtil.rangeWithMonth(DateUtil.parseFirstDayLocalDate(queryBo.getStartYm()), DateUtil.parseLastDayLocalDate(queryBo.getEndYm()));
    List&lt;SaleAmountHourHistory&gt; historyList = Lists.newArrayList();
    for (String ym : ymList) {
        SaleAmountHourHistory history = new SaleAmountHourHistory();
        history.setYear(Integer.parseInt(ym.split(&quot;-&quot;)[0]));
        history.setMonth(Integer.parseInt(ym.split(&quot;-&quot;)[1]));
        history.setYm(ym);

        history.setAmount(new BigDecimal(&quot;10000&quot;));
        history.setAmountSp(new BigDecimal(&quot;20000&quot;));
        history.setAmountLunarSp(new BigDecimal(&quot;30000&quot;));

        history.setSales(new BigDecimal(&quot;1000&quot;));
        history.setSalesSp(new BigDecimal(&quot;2000&quot;));
        history.setSalesLunarSp(new BigDecimal(&quot;3000&quot;));

        history.setCostPrice(new BigDecimal(&quot;100&quot;));
        history.setCostPriceSp(new BigDecimal(&quot;100&quot;));
        history.setCostPriceLunarSp(new BigDecimal(&quot;100&quot;));
        historyList.add(history);
    }

    return historyList;
} 

}

第二种,单元测试 {#第二种单元测试}

不需要启动项目,也不会连接数据库、RPC注 册中心等,但是相应的所有数据都需要打桩 Mock。

这种方法可以使用 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(&quot;123&quot;);

    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(&quot;123&quot;);
    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(&quot;1&quot;));
    when(productInfoSdk.getProductInfo(any())).thenReturn(new HashMap&lt;Long, Map&lt;String, String&gt;&gt;() {{
        put(1L, new HashMap&lt;String, String&gt;() {{
            put(&quot;String&quot;, &quot;String&quot;);
        }});
    }});

    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(&quot;allInPlateConfig&quot;);
    field.setAccessible(true);
    field.set(&quot;allInPlateConfig&quot;, new AllInPlateConfig());

    OrderQueue orderQueue = new OrderQueue();
    orderQueue.setSopOrderId(1111111L);
    DispatchResult result = checkAndFillProcessor.process(orderQueue);
    Assert.assertNotNull(result);
}

}

3、3.单元测试经验总结 {#33单元测试经验总结}

在工作中总结了一些单元测试的使用场景:

  1. 重构。如果我们拿到了一个代码,我们要去重构这个代码,如果这个代码本身的单元测试比较完善,那么我们重构完之后可以执行一下现有的单元测试,以保证重构前后代码在各个场景的逻辑保证最终一致,但是如果单元测试不完善甚至没有,那我建议大家可以基于AI去生成这个代码的单元测试,然后进行重构,再用生成的单元测试去把控质量,这里推荐 Diffblue 去生成,有兴趣的可以去了解一下。
  2. 新功能。新功能建议使用上面推荐的两种方法去做单测,第一种方法因为偏集成测试,单元测试代码编写的压力比较小,可以以黑盒测试的视角去覆盖测试 case 就可以了,但是如果某场景极为复杂,想要单独对某个复杂计算代码块进行专门的测试,那么可以使用第二种方法,第二种方法是很单纯的单元测试,聚焦专门代码块,但是如果普遍使用的话,单元测试代码编写量会很大,不建议单纯使用某一种,可以具体情况具体分析。

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


Ref:https://mp.weixin.qq.com/s/sTdyQtEXcp08OGypQQ3AHA

赞(4)
未经允许不得转载:工具盒子 » 一种极简单的 Spring Boot 单元测试方法