Spring Boot MockMvc测试遇NullPointerException问题求助
Spring Boot MockMvc测试空指针异常及Repository交互问题
问题现象
测试新增Musician接口时抛出空指针异常:
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is java.lang.NullPointerException: Cannot invoke "com.james.musician.model.Musician.getId()" because "savedMusician" is null
报错根源指向Controller的savedMusician.getId()调用:
at com.james.musician.controller.MusicianController.addMusician(MusicianController.java:68)
注释掉该代码后,又出现Repository未被调用的错误:
Wanted but not invoked: musicianRepository bean.save( <Capturing argument> ); -> at com.james.musician.controller.MusicianControllerMockMvcTest.addMusicianControllerTest(MusicianControllerMockMvcTest.java:93) Actually, there were zero interactions with this mock.
代码背景
测试代码
@SpringBootTest @AutoConfigureMockMvc class MusicianControllerMockMvcTest { @Autowired MusicianController musicianController; @MockBean MusicianRepository musicianRepo; @MockBean MusicianService musicianService; @Autowired private MockMvc mockMvc; @Autowired MusicianBuilder musicianBuilder; @Autowired ErrorValidation errorValidation; @Captor ArgumentCaptor<Musician> captor; @Test public void addMusicianControllerTest() throws Exception{ Musician musician = musicianBuilder.buildMusician(); Musician savedMusician = musicianBuilder.buildMusician(); savedMusician.setId(1L); ObjectMapper map = new ObjectMapper(); String jsonString = map.writeValueAsString(savedMusician); when(musicianService.createMusician(musician)).thenReturn(savedMusician); when(musicianRepo.save(musician)).thenReturn(savedMusician); this.mockMvc.perform(post("/api/musicians").contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(jsonString)).andDo(print()).andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value(1L)) .andExpect(jsonPath("$.msg").value("Musician Added Successfully")); verify(musicianRepo, new Times(1)).save(captor.capture()); Musician finalMusician = captor.getValue(); assertEquals(finalMusician.getEmailAddress(),savedMusician.getEmailAddress()); } }
Controller核心代码
@PostMapping public ResponseEntity addMusician(@RequestBody Musician musician) { AddResponse ad = new AddResponse(); try { Musician savedMusician = musicianService.createMusician(musician); String longIdToString = savedMusician.getId().toString(); HttpHeaders headers = new HttpHeaders(); headers.add("unique", longIdToString); ad.setMsg("Musician Added Successfully"); ad.setId(longIdToString); return new ResponseEntity<AddResponse>(ad, headers, HttpStatus.CREATED); } catch (MusicianException e) { ErrorMessage errorMessage = new ErrorMessage(e.getMessage()); return ResponseEntity.badRequest().body(errorMessage); } }
Service核心代码
@Service public class MusicianService { Musician musician; @Autowired MusicianRepository musicianRepo; @Autowired ErrorValidation errorValidation; public Musician createMusician(Musician musician) throws MusicianValidationException{ this.musician = musician; if(musicianRepo.findByEmailAddress(musician.getEmailAddress()) != null) { throw new MusicianValidationException(ErrorMessages.MUSISIAN_ALREADY_EXISTS.getMsg()); } if(musician.getStyle().equals(MusicStyle.FOLK)) { if(!errorValidation.checkValidFolkMusician(musician)) { throw new MusicianValidationException(ErrorMessages.INVALID_FOLK_MUSICIAN.getMsg()); } } if(errorValidation.checkDuplicateInstrument(musician)) { throw new MusicianValidationException(ErrorMessages.DUPLICATE_INSTRUMENTS.getMsg() + " (" + musician.getInstrumentA() + ")"); } if(!errorValidation.checkInstrumentComboAllowed(musician)) { throw new MusicianValidationException(ErrorMessages.INVALID_INSTRUMENT_COMBO.getMsg() + " (" + musician.getInstrumentA() + ", " + musician.getInstrumentB() + ")" ); } return musicianRepo.save(musician); } }
问题原因分析
- Mock匹配失败:测试中用
when(musicianService.createMusician(musician)).thenReturn(savedMusician)指定Mock行为,但实际请求传入的是JSON反序列化后的新Musician对象,和测试中创建的musician实例不是同一个,导致Mock未触发,savedMusician返回null。 - Repository Mock无效:
MusicianService被@MockBean替换成Mock对象后,其内部真实逻辑不会执行,即使Mock了musicianRepo.save(),也不会被调用,因此出现"zero interactions"错误。
解决方案
方案一:调整Mock匹配策略(Controller层测试推荐)
Controller层测试只需验证Controller与Service的交互,无需关心Service内部逻辑,直接Mock Service返回即可,无需Mock Repository:
@Test public void addMusicianControllerTest() throws Exception{ // 构建请求用的未保存Musician(不带ID) Musician requestMusician = musicianBuilder.buildMusician(); // 构建Service返回的已保存对象(带ID) Musician savedMusician = musicianBuilder.buildMusician(); savedMusician.setId(1L); ObjectMapper map = new ObjectMapper(); // 用请求对象生成JSON,贴近真实业务场景 String jsonString = map.writeValueAsString(requestMusician); // 使用any()匹配任意Musician参数,确保Mock触发 when(musicianService.createMusician(any(Musician.class))).thenReturn(savedMusician); this.mockMvc.perform(post("/api/musicians").contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(jsonString)) .andDo(print()) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value("1")) // 注意是字符串,AddResponse的id为String类型 .andExpect(jsonPath("$.msg").value("Musician Added Successfully")); // 验证Service方法被调用,捕获传入参数 verify(musicianService, times(1)).createMusician(captor.capture()); Musician capturedMusician = captor.getValue(); assertEquals(requestMusician.getEmailAddress(), capturedMusician.getEmailAddress()); }
关键调整点:
- 用
any(Musician.class)替代具体实例,避免对象不匹配导致Mock失效 - 删除对
musicianRepo的Mock和verify,Controller测试无需关心Repository交互 - 请求JSON使用未保存对象(不带ID),符合真实请求逻辑
- 匹配
jsonPath("$.id")时使用字符串,对应AddResponse的id类型
方案二:使用@SpyBean代替@MockBean(需验证Service与Repository交互时)
如果需要在Controller测试中同时验证Service与Repository的交互,将MusicianService的@MockBean改为@SpyBean,保留Service真实逻辑,同时Mock Repository方法:
@SpringBootTest @AutoConfigureMockMvc class MusicianControllerMockMvcTest { @Autowired MusicianController musicianController; @MockBean MusicianRepository musicianRepo; @SpyBean // 修改为SpyBean,保留Service真实逻辑 MusicianService musicianService; // 其他注入保持不变 @Test public void addMusicianControllerTest() throws Exception{ Musician requestMusician = musicianBuilder.buildMusician(); Musician savedMusician = musicianBuilder.buildMusician(); savedMusician.setId(1L); ObjectMapper map = new ObjectMapper(); String jsonString = map.writeValueAsString(requestMusician); // Mock Repository的findByEmailAddress返回null,避免触发重复异常 when(musicianRepo.findByEmailAddress(anyString())).thenReturn(null); // Mock Repository的save方法返回已保存对象 when(musicianRepo.save(any(Musician.class))).thenReturn(savedMusician); this.mockMvc.perform(post("/api/musicians").contentType(MediaType.APPLICATION_JSON) .accept(MediaType.APPLICATION_JSON) .content(jsonString)) .andDo(print()) .andExpect(status().isCreated()) .andExpect(jsonPath("$.id").value("1")) .andExpect(jsonPath("$.msg").value("Musician Added Successfully")); // 验证Repository的save方法被调用 verify(musicianRepo, times(1)).save(captor.capture()); Musician capturedMusician = captor.getValue(); assertEquals(requestMusician.getEmailAddress(), capturedMusician.getEmailAddress()); } }
关键调整点:
- 将
@MockBean MusicianService改为@SpyBean MusicianService,保留Service真实执行逻辑 - Mock Repository的
findByEmailAddress返回null,避免触发"已存在"异常 - 使用
any(Musician.class)匹配save方法参数,确保Mock生效
额外注意事项
- 确保
MusicianBuilder.buildMusician()生成的对象属性完整,避免因缺少必要属性导致验证失败 - 如果需要验证ErrorValidation的逻辑,可使用
@MockBean或@SpyBean控制其返回值
内容的提问来源于stack exchange,提问作者James Connolly
相关产品推荐
相关产品推荐

