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

前言

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

目录

1.POM依赖

2.单元测试类示例及注解释义

3.单元测试经验总结

一、POM依赖

Springboot版本: 2.6.6

xml 复制代码
<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

上代码:

less 复制代码
/**
 * @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 标识一个测试方法

上代码:

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

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

作者:京东零售 姜波

来源:京东云开发者社区

相关推荐
zopple2 小时前
常见的 Spring 项目目录结构
java·后端·spring
cjy0001114 小时前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
小江的记录本5 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji34165 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan5 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer7 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor3567 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor3567 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer7 小时前
Spring BOOT 启动参数
java·spring boot·后端
子木HAPPY阳VIP8 小时前
Ubuntu 22.04 VMware 设置固定IP配置
人工智能·后端·目标检测·机器学习·目标跟踪