控制器测试中UserService为空引发空指针异常的原因排查
UsersController测试抛出UserService空指针异常的原因及解决方法
问题描述
我在为UsersController编写测试用例,该控制器通过@RequiredArgsConstructor注入UserMapper和UserService。测试类中已使用@MockBean注解声明UserService和UserMapper,同时使用@InjectMocks注入UsersController,但运行测试时仍抛出NullPointerException,提示this.userService为null。
UsersController代码
@RestController @RequestMapping(URLConstants.User) @Log4j2 @RequiredArgsConstructor(onConstructor = @__({ @Autowired })) public class UsersController { private final UserMapper mapper; private final UserService userService; @PutMapping("/{id}") public UserDTO update(@PathVariable("id") Long id, @RequestBody UserUpdateRequest userRequest) { log.debug("PUT /users/" + id + " with data" + userRequest); User user = CurrentUserContext.getCurrentUser(); mapper.mergeToUser(userRequest, user); user = userService.update(user); return mapper.mapDTO(user); } }
UsersControllerTest代码
@SpringBootTest(classes = UsersController.class) @AutoConfigureMockMvc(addFilters = false) @ExtendWith(MockitoExtension.class) public class UsersControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserMapper userMapper; @MockBean private UserService userService; @Mock private CurrentUserContext currentUserContext; @InjectMocks private UsersController usersController; private final ObjectMapper objectMapper = new ObjectMapper(); @Test @DisplayName("Test successful user update") public void testUpdateUserSuccess() throws Exception { Long userId = 1L; UserUpdateRequest userUpdateRequest = new UserUpdateRequest(); userUpdateRequest.setFirstName("John"); userUpdateRequest.setLastName("Doe"); User updatedUser = new User(); updatedUser.setId(userId); updatedUser.setFirstName(userUpdateRequest.getFirstName()); updatedUser.setLastName(userUpdateRequest.getLastName()); updatedUser.setStatus(UserStatus.Active); updatedUser.setCreatedDate(new Timestamp(System.currentTimeMillis())); updatedUser.setUpdatedDate(new Timestamp(System.currentTimeMillis())); when(userService.update(updatedUser)).thenReturn(updatedUser); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(usersController).build(); mockMvc.perform(MockMvcRequestBuilders.put("/users/{id}", userId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(userUpdateRequest))) .andExpect(status().isOk()); } @Test @DisplayName("Test invalid user update request") public void testUpdateUserInvalidRequest() throws Exception { Long userId = 1L; UserUpdateRequest userUpdateRequest = new UserUpdateRequest(); MockMvc mockMvc = MockMvcBuilders.standaloneSetup(usersController).build(); mockMvc.perform(MockMvcRequestBuilders.put("/users/{id}", userId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(userUpdateRequest))) .andExpect(status().isBadRequest()); } }
异常栈信息
Jakarta.servlet.ServletException: Request processing failed: java.lang.NullPointerException: Cannot invoke "com.app.users.services.UserService.update(com.app.users.models.User)" because "this.userService" is null at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1019) ... Cannot invoke "com.app.users.services.UserService.update(com.app.users.models.User)" because "this.userService" is null java.lang.NullPointerException: Cannot invoke "com.app.users.services.UserService.update(com.app.users.models.User)" because "this.userService" is null at com.app.users.controllers.UsersController.update(UsersController.java:51) ...
原因分析
1. 混合使用Spring Boot测试与Mockito注入机制导致冲突
你同时启用了两种独立的测试上下文:
@SpringBootTest会启动Spring容器,将@MockBean声明的依赖注入到容器内的UsersController实例中;- 但你又通过
@InjectMocks创建了另一个独立的UsersController实例,并用MockMvcBuilders.standaloneSetup()启动该实例。由于@InjectMocks是Mockito的注解,无法获取Spring上下文里的@MockBean实例,最终导致这个独立实例的依赖未被注入。
2. @InjectMocks无法处理final字段的构造注入
UsersController的依赖是final类型,通过@RequiredArgsConstructor生成构造方法完成注入。Mockito的@InjectMocks对final字段支持有限,无法自动通过构造方法注入依赖,仅能尝试反射修改字段值,但final字段实例化后无法被修改,因此依赖注入失败。
解决方案
方案一:使用Spring Boot集成测试模式(推荐)
放弃手动创建独立的UsersController和MockMvc,直接使用Spring上下文自动配置的实例,确保依赖正确注入:
修改后的测试类代码:
@SpringBootTest(classes = UsersController.class) @AutoConfigureMockMvc(addFilters = false) public class UsersControllerTest { @Autowired private MockMvc mockMvc; @MockBean private UserMapper userMapper; @MockBean private UserService userService; private final ObjectMapper objectMapper = new ObjectMapper(); @Test @DisplayName("Test successful user update") public void testUpdateUserSuccess() throws Exception { Long userId = 1L; UserUpdateRequest userUpdateRequest = new UserUpdateRequest(); userUpdateRequest.setFirstName("John"); userUpdateRequest.setLastName("Doe"); User mockCurrentUser = new User(); mockCurrentUser.setId(userId); // Mock静态方法CurrentUserContext.getCurrentUser() try (MockedStatic<CurrentUserContext> mockedStatic = Mockito.mockStatic(CurrentUserContext.class)) { mockedStatic.when(CurrentUserContext::getCurrentUser).thenReturn(mockCurrentUser); User updatedUser = new User(); updatedUser.setId(userId); updatedUser.setFirstName(userUpdateRequest.getFirstName()); updatedUser.setLastName(userUpdateRequest.getLastName()); updatedUser.setStatus(UserStatus.Active); updatedUser.setCreatedDate(new Timestamp(System.currentTimeMillis())); updatedUser.setUpdatedDate(new Timestamp(System.currentTimeMillis())); when(userService.update(any(User.class))).thenReturn(updatedUser); when(userMapper.mapDTO(any(User.class))).thenReturn(new UserDTO()); mockMvc.perform(MockMvcRequestBuilders.put("/users/{id}", userId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(userUpdateRequest))) .andExpect(status().isOk()); } } @Test @DisplayName("Test invalid user update request") public void testUpdateUserInvalidRequest() throws Exception { Long userId = 1L; UserUpdateRequest userUpdateRequest = new UserUpdateRequest(); try (MockedStatic<CurrentUserContext> mockedStatic = Mockito.mockStatic(CurrentUserContext.class)) { mockedStatic.when(CurrentUserContext::getCurrentUser).thenReturn(new User()); mockMvc.perform(MockMvcRequestBuilders.put("/users/{id}", userId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(userUpdateRequest))) .andExpect(status().isBadRequest()); } } }
关键修改点:
- 移除
@ExtendWith(MockitoExtension.class)、@InjectMocks和手动创建MockMvc的代码; - 使用
Mockito.mockStatic()mock静态方法CurrentUserContext.getCurrentUser(),避免额外空指针; - 使用
any(User.class)匹配参数,避免对象equals不匹配导致mock失效。
方案二:使用Mockito独立测试模式
若不想启动Spring上下文,可改为纯Mockito测试,手动通过构造方法注入依赖:
修改后的测试类代码:
@ExtendWith(MockitoExtension.class) public class UsersControllerTest { @Mock private UserMapper userMapper; @Mock private UserService userService; private UsersController usersController; private MockMvc mockMvc; private final ObjectMapper objectMapper = new ObjectMapper(); @BeforeEach void setUp() { // 手动通过构造方法注入final字段依赖 usersController = new UsersController(userMapper, userService); mockMvc = MockMvcBuilders.standaloneSetup(usersController).build(); } @Test @DisplayName("Test successful user update") public void testUpdateUserSuccess() throws Exception { Long userId = 1L; UserUpdateRequest userUpdateRequest = new UserUpdateRequest(); userUpdateRequest.setFirstName("John"); userUpdateRequest.setLastName("Doe"); User mockCurrentUser = new User(); mockCurrentUser.setId(userId); try (MockedStatic<CurrentUserContext> mockedStatic = Mockito.mockStatic(CurrentUserContext.class)) { mockedStatic.when(CurrentUserContext::getCurrentUser).thenReturn(mockCurrentUser); User updatedUser = new User(); updatedUser.setId(userId); updatedUser.setFirstName(userUpdateRequest.getFirstName()); updatedUser.setLastName(userUpdateRequest.getLastName()); updatedUser.setStatus(UserStatus.Active); updatedUser.setCreatedDate(new Timestamp(System.currentTimeMillis())); updatedUser.setUpdatedDate(new Timestamp(System.currentTimeMillis())); when(userService.update(any(User.class))).thenReturn(updatedUser); when(userMapper.mapDTO(any(User.class))).thenReturn(new UserDTO()); mockMvc.perform(MockMvcRequestBuilders.put("/users/{id}", userId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(userUpdateRequest))) .andExpect(status().isOk()); } } @Test @DisplayName("Test invalid user update request") public void testUpdateUserInvalidRequest() throws Exception { Long userId = 1L; UserUpdateRequest userUpdateRequest = new UserUpdateRequest(); try (MockedStatic<CurrentUserContext> mockedStatic = Mockito.mockStatic(CurrentUserContext.class)) { mockedStatic.when(CurrentUserContext::getCurrentUser).thenReturn(new User()); mockMvc.perform(MockMvcRequestBuilders.put("/users/{id}", userId) .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(userUpdateRequest))) .andExpect(status().isBadRequest()); } } }
关键修改点:
- 移除
@SpringBootTest和@MockBean,改用@Mock声明依赖; - 在
@BeforeEach中手动通过构造方法创建UsersController实例,确保final字段被正确注入; - 同样需要mock
CurrentUserContext的静态方法。
内容的提问来源于stack exchange,提问作者David
相关产品推荐
相关产品推荐

