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

京东云开发者
• 阅读 93

前言

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

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

作者:京东零售 姜波

来源:京东云开发者社区

点赞
收藏
评论区
推荐文章
Wesley13 Wesley13
2年前
java单元测试工具
前言随着DevOp的不断流行,自动化测试慢慢成为Java开发者的关注点。因此,本文将分享10款优秀的单元测试框架和库,它们可以帮助Java开发人员在其Java项目上编写单元测试和集成测试。1\.JUnit我绝对JUnit不需要太多的介绍了。即使您是Java初学者,也可能听说过它。它可以帮助您为Java代码编写单元测试。几
云原生引擎单元测试实践
快速迭代的开发工作中如何提高代码质量一直是团队痛点,特别是没有测试支持的开发团队。合理的使用单元测试,并关注单元测试通过率、代码覆盖率可以有效提高代码质量。今天就来讲讲云原生引擎单元测试实践。
万木春 万木春
1年前
高效易用的C++单元测试框架:轻松构建高质量代码
单元测试是构建稳定、高质量的程序、服务或系统的必不可少的一环。通过单元测试,我们可以在开发过程中及时发现和修复代码中的问题,提高代码的质量和可维护性。同时,单元测试也可以帮助我们更好地理解代码的功能和实现细节,从而更好地进行代码重构和优化。
单元测试利器——手把手教你使用Mockito
作者:京东零售秦浩然从你成为开发人员的那一天起,写单元测试终究是你逃不开的宿命!那开发人员为什么不喜欢写单元测试呢?究其原因,无外乎是依赖。依赖其他的服务、依赖运行的环境、等等,各种依赖都成为了我们写单元测试的绊脚石。那现在有个单元测试利器可以帮我们解决依
Stella981 Stella981
2年前
OpenStack基础知识
针对以前学的内容的一个简单整理1、单元测试工具介绍unittest:是Python的标准库,提供了最基本的单元测试功能,包括单元测试运行器(简称runner)和单元测试框架。项目的单元测试代码的测试类可以继承unittest.TestCase类,那么这个类就能够被runner发现并且执行。同时,unittest.TestCase
Stella981 Stella981
2年前
Mock工具之Mockito实战
在实际项目中写单元测试的过程中我们会发现需要测试的类有很多依赖,这些依赖项又会有依赖,导致在单元测试代码里几乎无法完成构建,尤其是当依赖项尚未构建完成时会导致单元测试无法进行。为了解决这类问题我们引入了Mock的概念,简单的说就是模拟这些需要构建的类或者资源,提供给需要测试的对象使用。业内的Mock工具有很多,也已经很成熟了,这里我们将直接使用最流行的Moc
Stella981 Stella981
2年前
Spring Boot(十一):Spring Boot如何测试打包部署
一、开发阶段(一)单元测试在开发阶段的时候最重要的是单元测试,SpringBoot对单元测试的支持已经很完善了。1、在pom.xml文件中添加依赖<dependency<groupIdorg.springframework.boot</groupId<artifactIdspringb
Wesley13 Wesley13
2年前
Jacoco在eclipse上的集成使用
随着敏捷开发的流行,编写单元测试已经成为业界共识。但如何来衡量单元测试的质量呢?有些管理者片面追求单元测试的数量,导致底下的开发人员投机取巧,编写出大量的重复测试,数量上去了,质量却依然原地踏步。相比单纯追求单元测试的数量,分析单元测试的代码覆盖率是一种更为可行的方式。JaCoCo(JavaCodeCoverage)就是一种分析单元测试覆盖率的工具,使
Wesley13 Wesley13
2年前
Java单元测试总结
  单元测试的重要性这里就不说了,直接进入正题。很多程序员不喜欢写单元测试,导致项目经常会花很多时间去debug,这完全得不偿失。对关键方法进行单元测试,可以在早期业务逻辑还没那么复杂的时候,尽快排除症结。  在dao,manager,server,web这样的分层项目中,通常单元测试是要写在server层的,因为server层可以测的最多。本文中不介绍
Stella981 Stella981
2年前
OAuth 2.0 单元测试解决方案
为什么需要单元测试单元测试拥有保证代码质量、尽早发现软件Bug、简化调试过程、促进变化并简化集成、使流程更灵活等优势。单元测试是针对代码单元的独立测试,核心是“独立”,优势来源也是这种独立性,而所面临的不足也正是因为其独立性:既然是“独立”,就难以测试与其他代码和依赖环境的相互关系。单元测试与系统测试是互补而非代替关系。单元测试的优