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

Spring Boot Mockito问题:Mock授权方法失败引发空指针异常

问题原因

你用@InjectMocks注入的是真实的ApplicantDetailsService实例,Mockito的when()方法仅能作用于@Mock标注的模拟对象,对真实对象的方法无效。因此调用isAuthorizedForChange时会执行真实逻辑,而测试环境中SecurityContext里的Authentication为空,最终抛出空指针异常。

解决方案

以下三种方式都能实现你需要的测试场景,按推荐程度排序:

方案1:重构代码,拆分授权逻辑(推荐)

将授权校验逻辑抽成独立组件,符合单一职责原则,后续测试和维护更简单。

步骤1:创建独立的授权服务

@Service
public class UserAuthorizationService {
    public boolean isAuthorizedForChange(String username) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        UserDetails currentUser = (UserDetails) authentication.getPrincipal();
        return Objects.equals(username, currentUser.getUsername());
    }
}

步骤2:修改原服务类,注入授权服务

@Service
public class ApplicantDetailsService {
    private final ApplicantDetailsRepository applicantDetailsRepository;
    private final ApplicantDetailsMapper applicantDetailsMapper;
    private final UserAuthorizationService userAuthorizationService;

    // 构造方法注入所有依赖
    public ApplicantDetailsService(ApplicantDetailsRepository applicantDetailsRepository,
                                  ApplicantDetailsMapper applicantDetailsMapper,
                                  UserAuthorizationService userAuthorizationService) {
        this.applicantDetailsRepository = applicantDetailsRepository;
        this.applicantDetailsMapper = applicantDetailsMapper;
        this.userAuthorizationService = userAuthorizationService;
    }

    public ApplicantDetailsResponseDTO updateByUserID(Integer id, ApplicantDetailsRequestDTO newApplicantDetails) {
        ApplicantDetails applicantDetails = applicantDetailsRepository.findByUserId(id)
                .orElseThrow(() -> new EntityNotFoundException("Details of applicant with ID " + id + " not found"));

        // 调用独立的授权服务
        if(!userAuthorizationService.isAuthorizedForChange(applicantDetails.getUser().getUsername())) {
            return null;
        }

        applicantDetails.setFirstname(newApplicantDetails.getFirstname());
        applicantDetails.setLastname(newApplicantDetails.getLastname());
        applicantDetails.setPhoneNumber(newApplicantDetails.getPhoneNumber());

        ApplicantDetails updatedApplicantDetails = applicantDetailsRepository.save(applicantDetails);
        return applicantDetailsMapper.mapToApplicantDetailsResponseDTO(updatedApplicantDetails);
    }
}

步骤3:编写测试用例

直接mock授权服务的返回值即可:

@InjectMocks
private ApplicantDetailsService applicantDetailsService;
@Mock
private ApplicantDetailsRepository applicantDetailsRepository;
@Mock
private ApplicantDetailsMapper applicantDetailsMapper;
@Mock // 新增授权服务的mock
private UserAuthorizationService userAuthorizationService;

@BeforeEach
void setUp() {
    MockitoAnnotations.openMocks(this);
}

// 测试授权通过场景
@Test
void updateByUserID_authorized_returnsUpdatedDTO() {
    Integer userId = 1;
    ApplicantDetailsRequestDTO requestDTO = new ApplicantDetailsRequestDTO();
    ApplicantDetails applicantDetails = new ApplicantDetails();
    User user = new User();
    user.setUsername("test-user");
    applicantDetails.setUser(user);

    // mock repository和mapper逻辑
    Mockito.when(applicantDetailsRepository.findByUserId(userId)).thenReturn(Optional.of(applicantDetails));
    Mockito.when(applicantDetailsRepository.save(Mockito.any(ApplicantDetails.class))).thenReturn(applicantDetails);
    Mockito.when(applicantDetailsMapper.mapToApplicantDetailsResponseDTO(Mockito.any(ApplicantDetails.class)))
            .thenReturn(new ApplicantDetailsResponseDTO());

    // 指定授权通过
    Mockito.when(userAuthorizationService.isAuthorizedForChange("test-user")).thenReturn(true);

    // 执行测试
    ApplicantDetailsResponseDTO result = applicantDetailsService.updateByUserID(userId, requestDTO);

    // 断言结果
    Assertions.assertNotNull(result);
    Mockito.verify(applicantDetailsRepository).save(Mockito.any(ApplicantDetails.class));
}

// 测试授权失败场景
@Test
void updateByUserID_unauthorized_returnsNull() {
    Integer userId = 1;
    ApplicantDetailsRequestDTO requestDTO = new ApplicantDetailsRequestDTO();
    ApplicantDetails applicantDetails = new ApplicantDetails();
    User user = new User();
    user.setUsername("test-user");
    applicantDetails.setUser(user);

    Mockito.when(applicantDetailsRepository.findByUserId(userId)).thenReturn(Optional.of(applicantDetails));

    // 指定授权失败
    Mockito.when(userAuthorizationService.isAuthorizedForChange("test-user")).thenReturn(false);

    ApplicantDetailsResponseDTO result = applicantDetailsService.updateByUserID(userId, requestDTO);

    Assertions.assertNull(result);
    Mockito.verify(applicantDetailsRepository, Mockito.never()).save(Mockito.any(ApplicantDetails.class));
}

方案2:使用Mockito Spy部分模拟服务类

如果不想重构代码,可以用Spy包装真实服务对象,指定仅mockisAuthorizedForChange方法:

修改测试类配置

@Spy // 替换@InjectMocks为@Spy
private ApplicantDetailsService applicantDetailsService;
@Mock
private ApplicantDetailsRepository applicantDetailsRepository;
@Mock
private ApplicantDetailsMapper applicantDetailsMapper;

@BeforeEach
void setUp() {
    MockitoAnnotations.openMocks(this);
    // 手动将mock依赖注入到spy对象中
    applicantDetailsService.applicantDetailsRepository = applicantDetailsRepository;
    applicantDetailsService.applicantDetailsMapper = applicantDetailsMapper;
}

测试用例示例

注意必须用doReturn().when()语法,避免先执行真实方法:

@Test
void updateByUserID_authorized_returnsUpdatedDTO() {
    // 准备测试数据...

    // 指定isAuthorizedForChange返回true
    Mockito.doReturn(true).when(applicantDetailsService).isAuthorizedForChange(Mockito.anyString());

    // 执行测试和断言...
}

@Test
void updateByUserID_unauthorized_returnsNull() {
    // 准备测试数据...

    // 指定isAuthorizedForChange返回false
    Mockito.doReturn(false).when(applicantDetailsService).isAuthorizedForChange(Mockito.anyString());

    // 执行测试和断言...
}

方案3:手动设置SecurityContext

直接在测试中模拟登录用户,让真实的授权逻辑能正常执行:

@Test
void updateByUserID_authorized_returnsUpdatedDTO() {
    // 模拟登录用户
    UserDetails mockUser = Mockito.mock(UserDetails.class);
    Mockito.when(mockUser.getUsername()).thenReturn("test-user");
    Authentication authentication = Mockito.mock(Authentication.class);
    Mockito.when(authentication.getPrincipal()).thenReturn(mockUser);
    SecurityContextHolder.getContext().setAuthentication(authentication);

    // 准备测试数据、mock其他依赖...

    // 执行测试和断言...

    // 测试结束后清理上下文,避免影响其他测试
    SecurityContextHolder.clearContext();
}

@Test
void updateByUserID_unauthorized_returnsNull() {
    // 模拟登录其他用户
    UserDetails mockUser = Mockito.mock(UserDetails.class);
    Mockito.when(mockUser.getUsername()).thenReturn("other-user");
    Authentication authentication = Mockito.mock(Authentication.class);
    Mockito.when(authentication.getPrincipal()).thenReturn(mockUser);
    SecurityContextHolder.getContext().setAuthentication(authentication);

    // 准备测试数据(申请人用户名为test-user)...

    ApplicantDetailsResponseDTO result = applicantDetailsService.updateByUserID(1, new ApplicantDetailsRequestDTO());

    Assertions.assertNull(result);

    SecurityContextHolder.clearContext();
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 00:44:51