Mockito Strict Stubbing参数不匹配问题排查(仅调用一次when仍报错)
JUnit5+Mockito测试MealItemServiceImpl存根不匹配与空指针问题排查解决
问题现象
测试MealItemServiceImpl的createMealItem方法时,仅执行一次存根却触发PotentialStubbingProblem错误,提示存根参数不匹配;添加lenient()后又出现NullPointerException,明明设置了when().thenReturn()返回MealItem对象,却出现save方法返回值为null的情况。
错误原因分析
1. PotentialStubbingProblem错误原因
Mockito默认启用严格存根模式,存根时会严格匹配方法参数的引用或值:
- 测试代码中存根的是
mealItemRepository.save(mealItem),这里的mealItem是setup方法中创建的、带有id=9999L的对象。 - 但服务方法
createMealItem里会新建一个MealItem对象,这个对象的id为null,其余属性和测试中的mealItem一致。 - 两个对象是不同的引用,且
id属性值不同,导致存根参数与实际调用参数不匹配,触发严格模式的错误。
2. 添加lenient()后出现NPE的原因
lenient()会放宽Mockito的严格检查,但不会让不匹配的存根生效。实际调用save时传入的是服务里新建的id=null的MealItem,没有匹配的存根,Mockito会返回默认值null,后续调用save(mi).getId()自然触发空指针异常。
解决方案
核心思路是让存根匹配服务方法中实际传入的MealItem对象,以下是两种常用方式:
方式一:使用参数匹配器匹配任意MealItem对象
适合只关注save方法被调用、不严格校验参数属性的场景,用any(MealItem.class)作为存根和验证的参数:
@Test void normal_createMealItem(){ MealItemCreationDTO micDTO = new MealItemCreationDTO( "standard meal", LocalDate.parse("2023-06-09"), LocalTime.parse("14:35:00"), MealItem.MealSize.heavy, "test note" ); // 存根匹配任意MealItem对象,返回预设的mealItem when(mealItemRepository.save(any(MealItem.class))).thenReturn(mealItem); Long returnId = mealItemService.createMealItem(user, micDTO); // 验证save被调用一次,参数为任意MealItem verify(mealItemRepository, times(1)).save(any(MealItem.class)); assertEquals(mealItem.getId(), returnId); }
方式二:使用参数匹配器精确校验属性
如果需要确保传入的MealItem属性符合预期,可以用argThat自定义匹配逻辑:
@Test void normal_createMealItem(){ MealItemCreationDTO micDTO = new MealItemCreationDTO( "standard meal", LocalDate.parse("2023-06-09"), LocalTime.parse("14:35:00"), MealItem.MealSize.heavy, "test note" ); // 存根时校验传入的MealItem属性 when(mealItemRepository.save(argThat(mi -> mi.getUserId().equals(user.getId()) && mi.getMeal().equals(micDTO.meal()) && mi.getDate().equals(micDTO.date()) && mi.getTime().equals(micDTO.time()) && mi.getMealSize().equals(micDTO.mealSize()) && mi.getNote().equals(micDTO.note()) && mi.getId() == null // 服务中新建的对象id为null ))).thenReturn(mealItem); Long returnId = mealItemService.createMealItem(user, micDTO); // 验证时同样使用相同的匹配逻辑 verify(mealItemRepository, times(1)).save(argThat(mi -> mi.getUserId().equals(user.getId()) && mi.getMeal().equals(micDTO.meal()) && mi.getDate().equals(micDTO.date()) && mi.getTime().equals(micDTO.time()) && mi.getMealSize().equals(micDTO.mealSize()) && mi.getNote().equals(micDTO.note()) && mi.getId() == null )); assertEquals(mealItem.getId(), returnId); }
补充说明
- 如果
MealItem类重写了equals和hashCode方法(基于除id外的业务属性),也可以直接用eq匹配器,但需要确保测试中构造一个和服务里属性完全一致(除id)的对象来存根。 - 避免依赖对象引用匹配,测试中尽量使用参数匹配器来适配服务内部创建的对象,减少测试代码与实现细节的耦合。
内容的提问来源于stack exchange,提问作者wandering_scholar
相关产品推荐
相关产品推荐

