一种极简单的 Spring Boot 单元测试方法
本文主要介绍了一种单元测试方法,力求零基础人员可以从本文中受到启发,可以搭建一套好用的单元测试环境,并能切实提高交付代码的质量。极简体现在除了 POM 依赖和单元测试类之外,其他什么都不需要引入,只需要一个本地能启动的 Spring Boot 项目。
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)
启动了一套 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("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、3.单元测试经验总结
在工作中总结了一些单元测试的使用场景:
- 重构。如果我们拿到了一个代码,我们要去重构这个代码,如果这个代码本身的单元测试比较完善,那么我们重构完之后可以执行一下现有的单元测试,以保证重构前后代码在各个场景的逻辑保证最终一致,但是如果单元测试不完善甚至没有,那我建议大家可以基于AI去生成这个代码的单元测试,然后进行重构,再用生成的单元测试去把控质量,这里推荐
Diffblue
去生成,有兴趣的可以去了解一下。 - 新功能。新功能建议使用上面推荐的两种方法去做单测,第一种方法因为偏集成测试,单元测试代码编写的压力比较小,可以以黑盒测试的视角去覆盖测试 case 就可以了,但是如果某场景极为复杂,想要单独对某个复杂计算代码块进行专门的测试,那么可以使用第二种方法,第二种方法是很单纯的单元测试,聚焦专门代码块,但是如果普遍使用的话,单元测试代码编写量会很大,不建议单纯使用某一种,可以具体情况具体分析。
建议大家做单元测试不要单纯的追求行覆盖率,还是要本着提高质量的心态去做单元测试。
Ref:https://mp.weixin.qq.com/s/sTdyQtEXcp08OGypQQ3AHA