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

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); 
    }
}

问题原因分析

  1. Mock匹配失败:测试中用when(musicianService.createMusician(musician)).thenReturn(savedMusician)指定Mock行为,但实际请求传入的是JSON反序列化后的新Musician对象,和测试中创建的musician实例不是同一个,导致Mock未触发,savedMusician返回null。
  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 05:05:01