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

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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 16:40:56