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

控制器测试中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字段被正确注入;
  • 同样需要mockCurrentUserContext的静态方法。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 18:52:05