Spring Boot测试MockMvc空JSON及JdbcTemplate Bean缺失问题排查
问题描述
基于java-faker构建随机人员数据项目,用于学习Java、Spring、API、Thymeleaf及软件测试技术。项目主体运行正常,但使用JUnit+Spring MockMvc进行测试时,MockMvc调用接口后返回的JSON为空;启动测试时还出现如下错误:
*************************** APPLICATION FAILED TO START *************************** Description: Field jdbcTemplate in com.example.demo.controller.PersonController required a bean of type 'org.springframework.jdbc.core.JdbcTemplate' that could not be found.
测试代码
package com.example.demo; import com.example.demo.controller.PersonController; import com.example.demo.entity.Person; import com.example.demo.service.PersonService; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.http.MediaType; import org.springframework.test.context.event.annotation.AfterTestClass; import org.springframework.test.context.event.annotation.BeforeTestClass; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; import org.springframework.test.web.servlet.result.MockMvcResultHandlers; import org.springframework.test.web.servlet.result.MockMvcResultMatchers; import org.springframework.util.Assert; import java.util.ArrayList; import java.util.List; import static org.springframework.test.util.AssertionErrors.assertEquals; @WebMvcTest(PersonController.class) @AutoConfigureMockMvc //@SpringBootTest //@ExtendWith(MockitoExtension.class) public class PersonTest { @Autowired private MockMvc mockMvc; @Mock private PersonController personController; @InjectMocks PersonService personService; static final String url = "http://localhost:8000"; //static final String urlCreateData = "http://localhost:8000/createMoreData?username=admin&pw=secret"; static final String urlCreateData = "http://localhost:8000?username=admin&pw=secret"; private static final Logger log = LoggerFactory.getLogger(PersonTest.class); private List<Person> personList; @DisplayName("Start -> Request") @BeforeTestClass public void start() throws Exception { log.info("Start -> Request"); mockMvc.perform(MockMvcRequestBuilders .get(urlCreateData) .accept(MediaType.APPLICATION_JSON)); personList.addAll(PersonService.createNewData(10)); } @Test @DisplayName("Test. Get new person list.size") void testPersonObjectList(){ //when(PersonService.createNewData(10)). assertEquals( "Test: Create new list with objects with size of 10", 10, PersonService.createNewData(10).size()); } @Test @DisplayName("Test. Get new person != null") void testPersonObjectNotNull(){ Assert.notNull( PersonService.createNewData().get(0), "Test: Get new person != null"); } @Test public void testPersonListCountObjects(){ List<Person> personList = new ArrayList<>(PersonService.createNewData(10)); assertEquals("Created new list with 10 objects", 10, personList.size()); personList.addAll(PersonService.createNewData(10)); assertEquals("Expanded list with 10 more objects", 20, personList.size()); } @Test public void testStatusOk() throws Exception { mockMvc.perform(MockMvcRequestBuilders .get(url) .accept(MediaType.APPLICATION_JSON) ) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) ; } @Test public void testStatusOkAndJsonPathExists() throws Exception { mockMvc.perform(MockMvcRequestBuilders .get(url) .accept(MediaType.APPLICATION_JSON) ) .andDo(MockMvcResultHandlers.print()) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.jsonPath("$."+personService.NAME_FOR_MODEL_DATA).exists()) ; } @AfterTestClass @DisplayName("End -> Clean") public void clean() { log.info("End -> Clean"); personService.setCREATE_DATA_FOR_TEST(false); } }
控制器代码
package com.example.demo.controller; import com.example.demo.entity.Person; import com.example.demo.repository.PersonRepository; import com.example.demo.service.PersonService; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.core.env.Environment; import org.springframework.http.HttpStatus; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Service; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.ModelAndView; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import java.util.List; import java.util.Optional; import static com.example.demo.service.PersonService.*; @RestController //@Controller //@RequestMapping("/index.html") //@RequestMapping("/") @RequestMapping("") @EnableWebMvc public class PersonController { private static final Logger log = LoggerFactory.getLogger(PersonController.class); @Autowired Environment environment; @Autowired JdbcTemplate jdbcTemplate; @Autowired PersonRepository personRepository; @Autowired PersonService personService; final String htmlFile = "index"; @PostMapping("/createMoreData") @ResponseStatus(code = HttpStatus.OK) public ModelAndView createMoreData( @RequestParam(required = false) String username, @RequestParam(required = false) String pw, Model model, HttpServletResponse httpServletResponse ){ ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName(htmlFile); switch (isAdminAccount(username, pw)) { case YES -> { if(isDatabaseEmpty()) { createNewData(true); model.addAttribute("message", "Created new data with admin account(s)."); } else { createNewData(); model.addAttribute("message", "Created new data."); } model.addAttribute(personService.NAME_FOR_MODEL_DATA, getDataWithoutSensibleInfos(true, personRepository.findAll())); model.addAttribute(personService.NAME_FOR_MODEL_PERMISSION, true); } case EMPTY_PARAMETER -> { log.error(PersonService.IsAdmin.EMPTY_PARAMETER.toString()); model.addAttribute(personService.NAME_FOR_MODEL_MESSAGE, "Did not create new data: " + PersonService.IsAdmin.EMPTY_PARAMETER.toString()); httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); model.addAttribute(personService.NAME_FOR_MODEL_DATA, getDataWithoutSensibleInfos(false, personRepository.findAll())); model.addAttribute(personService.NAME_FOR_MODEL_PERMISSION, false); } case WRONG_PARAMETER -> { log.error(PersonService.IsAdmin.WRONG_PARAMETER.toString()); model.addAttribute(personService.NAME_FOR_MODEL_MESSAGE, "Did not create new data: " + PersonService.IsAdmin.WRONG_PARAMETER.toString()); httpServletResponse.setStatus(HttpServletResponse.SC_BAD_REQUEST); model.addAttribute(personService.NAME_FOR_MODEL_DATA, getDataWithoutSensibleInfos(false, personRepository.findAll())); model.addAttribute(personService.NAME_FOR_MODEL_PERMISSION, false); } } return modelAndView; } @GetMapping("") public ModelAndView loadData( @RequestParam(required = false) String username, @RequestParam(required = false) String pw, Model model ){ ModelAndView modelAndView = new ModelAndView(); modelAndView.setViewName(htmlFile); switch (isAdminAccount(username, pw)) { case YES -> { log.info(IsAdmin.YES.toString()); if(personService.CREATE_DB_DATA_ON_STARTUP && isDatabaseEmpty()) { createNewData(true); model.addAttribute(personService.NAME_FOR_MODEL_MESSAGE, "Created new data with admin account(s)."); } model.addAttribute(personService.NAME_FOR_MODEL_DATA, getDataWithoutSensibleInfos(true, personRepository.findAll())); } case EMPTY_PARAMETER -> { log.error(IsAdmin.EMPTY_PARAMETER.toString()); model.addAttribute(personService.NAME_FOR_MODEL_PERMISSION, false); model.addAttribute(personService.NAME_FOR_MODEL_MESSAGE, IsAdmin.EMPTY_PARAMETER.toString()); model.addAttribute(personService.NAME_FOR_MODEL_DATA, getDataWithoutSensibleInfos(false, personRepository.findByIsAdminFalse())); } case WRONG_PARAMETER -> { log.error(IsAdmin.WRONG_PARAMETER.toString()); model.addAttribute(personService.NAME_FOR_MODEL_PERMISSION, false); model.addAttribute(personService.NAME_FOR_MODEL_MESSAGE, IsAdmin.WRONG_PARAMETER.toString()); model.addAttribute(personService.NAME_FOR_MODEL_DATA, getDataWithoutSensibleInfos(false, personRepository.findByIsAdminFalse())); } } return modelAndView; } private void createNewDataIfNotCreated(){ if(isDatabaseEmpty()){ List<Person> personList = createNewDataWithAdmin(); log.info("Saving all "+personList.size()+" data to database."); personRepository.saveAll(personList); } } private void createNewData(){ createNewData(false); } private void createNewData(boolean withAdmin){ List<Person> personList = null; if(withAdmin) { personList = createNewDataWithAdmin(); } else { personList = PersonService.createNewData(); } log.info("Saving all " + personList.size() + " data to database."); personRepository.saveAll(personList); } private boolean isDatabaseEmpty(){ return jdbcTemplate.queryForList("select * from " + personService.DATABASE_NAME + " limit 1;").isEmpty(); } }
解决方案
1. 修复JdbcTemplate Bean找不到的问题
- 原因:
@WebMvcTest仅加载Web层组件,不会自动配置数据库相关Bean(JdbcTemplate、Repository等),而控制器依赖这些Bean,导致启动失败。 - 方案选择:
- 集成测试:改用
@SpringBootTest+@AutoConfigureMockMvc,加载完整Spring上下文,适合测试完整流程:@SpringBootTest @AutoConfigureMockMvc public class PersonTest { @Autowired private MockMvc mockMvc; @Autowired private PersonService personService; // ... 测试代码 } - 单元测试:如果仅测试Web层,用
@MockBean为控制器依赖的Bean创建Mock实例,加入Spring上下文:@WebMvcTest(PersonController.class) public class PersonTest { @Autowired private MockMvc mockMvc; @MockBean private JdbcTemplate jdbcTemplate; @MockBean private PersonRepository personRepository; @MockBean private PersonService personService; // ... 测试代码 }
- 集成测试:改用
2. 修复MockMvc返回JSON为空的问题
- 原因:控制器标注了
@RestController却返回ModelAndView(用于Thymeleaf视图渲染),请求指定accept(MediaType.APPLICATION_JSON)时,实际返回的是HTML内容,而非JSON,导致JSON路径校验失败。 - 方案:
- 调整控制器:如果是API接口,改为返回
@ResponseBody或直接返回对象,让Spring自动序列化为JSON:@GetMapping("") @ResponseBody public Map<String, Object> loadData(...) { // 构建返回数据并返回 } - 调整测试逻辑:如果是测试视图渲染,验证模型中的数据而非JSON:
mockMvc.perform(MockMvcRequestBuilders.get(url) .param("username", "admin") .param("pw", "secret")) .andExpect(MockMvcResultMatchers.status().isOk()) .andExpect(MockMvcResultMatchers.model().attributeExists(personService.NAME_FOR_MODEL_DATA));
- 调整控制器:如果是API接口,改为返回
3. 修复测试代码中的其他问题
@BeforeTestClass和@AfterTestClass方法必须是static,否则会报错;同时personList需要先初始化,避免NullPointerException:private static List<Person> personList; @BeforeTestClass public static void start() throws Exception { log.info("Start -> Request"); // ... mockMvc请求代码 personList = new ArrayList<>(); personList.addAll(PersonService.createNewData(10)); }- 删除
@Mock private PersonController personController,@WebMvcTest已经自动加载了控制器实例,Mock它会覆盖真实控制器,导致测试无效。 - 避免直接调用静态方法
PersonService.createNewData(),改用注入的personService实例,并通过Mockito Mock方法返回值,提升测试可控性:when(personService.createNewData(10)).thenReturn(Collections.singletonList(new Person()));
内容的提问来源于stack exchange,提问作者kasper2083
相关产品推荐
相关产品推荐

