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
相关产品推荐
相关产品推荐

