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

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

【本文目录】
  • POM依赖
  • 单元测试类示例
  • 单元测试经验总结
1.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>
2.单元测试类示例



主要有两种第一种,偏集成测试需要启动项目,需要连接数据库、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快速生成单元测试类的框架

主要注解:@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); }}
3.单元测试经验总结



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

1. 重构。如果我们拿到了一个代码,我们要去重构这个代码,如果这个代码本身的单元测试比较完善,那么我们重构完之后可以执行一下现有的单元测试,以保证重构前后代码在各个场景的逻辑保证最终一致,但是如果单元测试不完善甚至没有,那我建议大家可以基于AI去生成这个代码的单元测试,然后进行重构,再用生成的单元测试去把控质量,这里推荐Diffblue去生成,有兴趣的可以去了解一下。

2. 新功能。新功能建议使用上面推荐的两种方法去做单测,第一种方法因为偏集成测试,单元测试代码编写的压力比较小,可以以黑盒测试的视角去覆盖测试case就可以了,但是如果某场景极为复杂,想要单独对某个复杂计算代码块进行专门的测试,那么可以使用第二种方法,第二种方法是很单纯的单元测试,聚焦专门代码块,但是如果普遍使用的话,单元测试代码编写量会很大,不建议单纯使用某一种,可以具体情况具体分析。

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

-    END    -



相关推荐

  • 白鲸开源CEO郭炜:数据集成的未来在哪里?
  • 美好生活,从情绪开始
  • 超过20W个高质量组件的开源PCB库
  • 11.7K Star极客工具!HTTP请求还能这样简单定义
  • 掌握这8个方法,精通SQL日期函数
  • 李彦宏:已有10%的大搜流量通过文心一言模型生成;微软向 Rust 基金会捐赠 100 万美元 | 极客头条
  • 大模型之战的下半场:行业应用怎么做?
  • CloudWeGo 技术沙龙 · 上海站报名开启,共议云原生 x AI 时代的微服务架构最佳实践
  • 超 50 万人「退休金」数据丢失、1250 亿澳元资产不可见,Google Cloud 因配置错误险酿大祸!
  • 从零开始手搓 GPU,照着英伟达 CUDA 来,只用两个星期
  • 第一批用 LangChain 的程序员,已经碾压同事了。。。
  • 偷偷爆料下银行信息科技岗的薪资。。
  • 拾日谈:“到今天我也没弄明白,怎么成为一个优秀投资人”
  • 程序员求职越来越难
  • 前端苦HTML+CSS久已
  • 搞了3年开源,年收入超200万,什么水平?
  • 网络实验新境界,PNETLab模拟器部署指南
  • 在公司先后2次晋升失败,出去面试最终拿到涨薪30%的offer。结果领导说:你过去也是从头开始,不如留下来,这次肯定能升主管!
  • 广州某小厂后端面试(首面)
  • 很穷的人怎么年入100万?