You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 07:44:55