首页 / Spring Boot 入门教程 / Web 层测试

Spring Boot 入门教程

Web 层测试

本教程共 48 篇 · 第 40 篇 · 更新于 2026-08-13 · 约 3 分钟阅读

Spring Boot@WebMvcTestMockMvc接口测试JSON断言切片测试REST

本节目标:用 @WebMvcTest 只测控制器层,用 MockMvc 模拟请求并断言响应,学会 JSON 断言和真实端口测试。

为什么用切片测试

上一节的 @SpringBootTest 会启动整个应用上下文。测一个控制器,却把数据库、消息队列全拉起来了,又慢又脆。

切片测试(Slice Test)只加载你关心的那部分。测 Web 层就用 @WebMvcTest,它只装配 MVC 相关组件:

  • @Controller@RestController
  • @ControllerAdvice
  • 过滤器、拦截器、ConverterHandlerMethodArgumentResolver

普通 @Component@Service@ConfigurationProperties 不会被扫描。所以控制器依赖的 Service 得用替身,这正是上一节学的 @MockitoBean 派上用场的地方。

第一个 MockMvc 测试

假设有这样一个控制器:

package com.example.demo.hello;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @GetMapping("/hello")
    public String hello() {
        return "Hello World";
    }
}

对应的测试:

package com.example.demo.hello;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.test.web.servlet.MockMvc;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(HelloController.class)
class HelloControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    void helloReturnsText() throws Exception {
        mvc.perform(get("/hello"))            // 发起 GET 请求
                .andExpect(status().isOk())          // 状态码 200
                .andExpect(content().string("Hello World")); // 响应体
    }
}

MockMvc 不启动真实服务器,它在 Spring MVC 层直接「模拟」请求和响应。速度快,毫秒级完成。

Note

Spring Boot 4.x 里 @WebMvcTest 的包名是 org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest,旧教程里的 org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest 是 3.x 写法,别照抄。

有依赖的控制器:@MockitoBean

控制器通常调 Service。切片不加载 Service,用替身顶上:

package com.example.demo.user;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mvc;

    @MockitoBean
    private UserService userService;

    @Test
    void getUserReturnsJson() throws Exception {
        given(userService.findNameById(1L)).willReturn("小明");

        mvc.perform(get("/users/1").accept(MediaType.APPLICATION_JSON))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("小明"));
    }
}

jsonPath("$.name") 表示取 JSON 里 name 字段。$ 是根节点,$.name 就是根节点下的 name

JSON 断言三件套

断言接口返回的 JSON,常用三种方式:

方式一:jsonPath 匹配器,适合单字段验证,上面例子就是。

方式二:JSONassert 整段对比,适合结构化的完整响应:

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;

mvc.perform(get("/users/1"))
        .andExpect(content().json("""
                {"id": 1, "name": "小明"}
                """));

方式三:反序列化后断言,把响应体转成对象再查字段:

import tools.jackson.databind.ObjectMapper;

String body = mvc.perform(get("/users/1"))
        .andReturn().getResponse().getContentAsString();

User user = new ObjectMapper().readValue(body, User.class);
assertThat(user.getName()).isEqualTo("小明");

三种方式各有用处:单字段用 jsonPath,整体结构用 JSONassert,要拿对象继续操作就用 ObjectMapper(4.x 起 Jackson 3 换包名 tools.jackson)。

提交数据:POST 测试

import org.springframework.http.MediaType;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Test
void createUserAcceptsJson() throws Exception {
    mvc.perform(post("/users")
                    .contentType(MediaType.APPLICATION_JSON)
                    .content("""
                            {"name": "小红"}
                            """))
            .andExpect(status().isCreated());
}

注意 contentTypecontent 配套:声明发的是 JSON,再给 JSON 字符串。漏了 contentType,Spring 可能按表单解析,@RequestBody 收不到数据。

AssertJ 风格:MockMvcTester

MockMvc 的 andExpect 用的是匹配器风格。4.x 还提供 MockMvcTester,走 AssertJ 链式断言:

import org.springframework.test.web.servlet.assertj.MockMvcTester;

import static org.assertj.core.api.Assertions.assertThat;

@WebMvcTest(HelloController.class)
class HelloControllerTesterTest {

    @Autowired
    private MockMvcTester mvc;

    @Test
    void helloReturnsText() {
        assertThat(mvc.get().uri("/hello"))
                .hasStatusOk()
                .hasBodyTextEqualTo("Hello World");
    }
}

习惯 AssertJ 的话,MockMvcTester 读起来更顺,两个 API 可以共存,选一个用到底就行。

@SpringBootTest 里也能用 MockMvc

不想切片、想加载完整上下文又不想开端口,用 @SpringBootTest@AutoConfigureMockMvc

import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;

@SpringBootTest
@AutoConfigureMockMvc
class FullContextWebTest {
    // 注入 MockMvc,其余同前
}

真实端口测试:RANDOM_PORT + TestRestTemplate

MockMvc 摸不到真实服务器行为(比如容器级错误页、连接层问题)。要真刀真枪,用 RANDOM_PORT 启动内嵌服务器:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.TestRestTemplate;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.http.ResponseEntity;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class HelloControllerPortTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void helloOverRealServer() {
        ResponseEntity<String> response =
                restTemplate.getForEntity("/hello", String.class);

        assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
        assertThat(response.getBody()).isEqualTo("Hello World");
    }
}

RANDOM_PORT 每次测试随机选一个空闲端口,避免和本机服务冲突。URL 不写主机和端口,TestRestTemplate 自动连到测试服务器。

@JsonTest:单独测 JSON 序列化

只关心对象和 JSON 的转换,用 @JsonTest。它自动配置 Jackson,并支持注入 JacksonTester

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;

import static org.assertj.core.api.Assertions.assertThat;

@JsonTest
class UserJsonTest {

    @Autowired
    private JacksonTester<User> json;

    @Test
    void serialize() throws Exception {
        User user = new User(1L, "小明");

        assertThat(json.write(user))
                .hasJsonPathStringValue("$.name")
                .extractingJsonPathStringValue("$.name")
                .isEqualTo("小明");
    }
}

序列化规则(字段命名、忽略 null、日期格式)变了,这里最先暴露问题。

Tip

测试策略搭配:@WebMvcTest 测接口行为,@JsonTest 测序列化,RANDOM_PORT 测试做关键链路的端到端验证。层越薄跑得越快,能切片就别全家桶。

小结

  • @WebMvcTest 只加载 MVC 层,配合 @MockitoBean 隔离 Service。
  • MockMvc 不启动服务器,perform + andExpect 完成请求断言。
  • JSON 断言:jsonPath 单字段、JSONassert 整段、ObjectMapper 转对象。
  • 要真实服务器行为,用 RANDOM_PORT + TestRestTemplate。4.x 也可用新的 REST Test Client(第 46 章有总览),旧写法仍可用。