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

Mockito中Optional.map().orElseThrow()的Mock失效问题排查

Mockito Mock在Optional.map链式调用中未生效问题排查

问题场景

在Udemy课程的单元测试中,测试ProjectService的createGroup方法时遇到异常:尽管已经mock了projectRepository.findById返回存在的Project,但执行时仍抛出java.util.NoSuchElementException: No project with ID 99 found。

待测试的原方法

public GroupReadModel createGroup(LocalDateTime deadline, Integer projectId) {
    if (!configurationProperties.getTemplate().isAllowMultipleTasks() && taskGroupRepository.existsByDoneIsFalseAndProject_Id(projectId)) {
        throw new IllegalStateException("Only one undone group form project is allowed");
    }

    TaskGroup result = projectRepository.findById(projectId)
            .map(project -> {
                TaskGroup taskGroup = new TaskGroup();
                taskGroup.setDescription(project.getDescription());
                taskGroup.setTasks(project.getProjectSteps().stream()
                        .map(step -> Task.createNewTask(step.getDescription(), deadline.plusDays(step.getDaysToDeadline())))
                        .collect(Collectors.toSet()));
                taskGroup.setProject(project);
                return taskGroupRepository.save(taskGroup);
            }).orElseThrow(() -> new NoSuchElementException(String.format("No project with ID %d found", projectId)));

    return new GroupReadModel(result);
}

测试方法

@ExtendWith(SpringExtension.class)
class ProjectServiceTest {

    @Autowired
    private ProjectService projectService;

    @MockBean
    private ProjectRepository projectRepository;
    @MockBean
    private TaskGroupRepository taskGroupRepository;
    @MockBean
    private TaskConfigurationProperties configurationProperties;
    @Mock
    private TaskConfigurationProperties.Template template;

    @TestConfiguration
    static class ProjectServiceTestConfig {
        @Bean
        ProjectService projectService(ProjectRepository projectRepository, TaskGroupRepository taskGroupRepository, TaskConfigurationProperties configurationProperties ){
            return new ProjectService(projectRepository, taskGroupRepository, configurationProperties);
        }
    }


    @Test
    void should_return_new_group_read_model() {
        //given
        LocalDateTime deadline = LocalDateTime.now();
        Integer projectId = 99;
        Project projectById = new Project(projectId, "test project");
        projectById.setProjectSteps(Set.of(new ProjectStep("test1", 2)));
        TaskGroup taskGroupSaved = TaskGroup.CreateNewTaskGroup(projectById.getDescription(), Set.of(Task.createNewTask("test1", LocalDateTime.now())));
        GroupReadModel expectedResult = new GroupReadModel(taskGroupSaved);
        expectedResult.setDeadline(expectedResult.getDeadline().plusDays(2));
        Mockito.when(configurationProperties.getTemplate()).thenReturn(template);
        Mockito.when(template.isAllowMultipleTasks()).thenReturn(true);
        Mockito.when(taskGroupRepository.existsByDoneIsFalseAndProject_Id(projectId)).thenReturn(false);
        Mockito.when(projectRepository.findById(projectId)).thenReturn(Optional.of(projectById));

        //when
        GroupReadModel result = projectService.createGroup(deadline, projectId);

        //then
        assertEquals(expectedResult, result);
    }
}

特殊现象

将原方法拆分为先获取Project再处理的写法后,mock能正常工作,不会抛出异常:

Project projectById = projectRepository.findById(projectId)
        .orElseThrow(() -> new NoSuchElementException(String.format("No project with ID %d found", projectId)));
TaskGroup taskGroup = new TaskGroup();
taskGroup.setDescription(projectById.getDescription());
taskGroup.setTasks(projectById.getProjectSteps().stream()
        .map(step -> Task.createNewTask(step.getDescription(), deadline.plusDays(step.getDaysToDeadline())))
        .collect(Collectors.toSet()));
taskGroup.setProject(projectById);
taskGroupRepository.save(taskGroup);

原因分析

问题的核心在于**taskGroupRepository.save方法的mock缺失**:

  • 在原方法的Optional.map链式调用中,map的lambda表达式最终返回taskGroupRepository.save(taskGroup)的结果。
  • Mockito中未mock的方法默认返回null,所以save调用返回null,而Optional.map会将映射函数返回的null转换为Optional.empty()。
  • 后续的orElseThrow遇到Optional.empty()就会触发预设的NoSuchElementException,看起来像是projectRepository.findById的mock未生效,但实际上是map阶段导致整个Optional变为空。

而拆分写法中,代码并没有将save的返回值作为最终的TaskGroup结果,orElseThrow在获取Project阶段就已经执行完成,所以即使save返回null也不会触发异常。

解决方案

在测试代码的given阶段添加对taskGroupRepository.save的mock,指定它返回预设的taskGroupSaved对象:

// 在given部分添加这一行
Mockito.when(taskGroupRepository.save(Mockito.any(TaskGroup.class))).thenReturn(taskGroupSaved);

添加后,map函数中的save调用会返回预设的TaskGroup实例,Optional不会变为空,orElseThrow就不会触发,测试就能正常执行。

如果需要验证save方法确实被调用,可以在then阶段添加验证:

Mockito.verify(taskGroupRepository).save(Mockito.any(TaskGroup.class));

内容的提问来源于stack exchange,提问作者Pioter88

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 13:21:24