Web 层测试
本教程共 48 篇 · 第 40 篇 · 更新于 2026-08-13 · 约 3 分钟阅读
本节目标:用
@WebMvcTest只测控制器层,用 MockMvc 模拟请求并断言响应,学会 JSON 断言和真实端口测试。
为什么用切片测试
上一节的 @SpringBootTest 会启动整个应用上下文。测一个控制器,却把数据库、消息队列全拉起来了,又慢又脆。
切片测试(Slice Test)只加载你关心的那部分。测 Web 层就用 @WebMvcTest,它只装配 MVC 相关组件:
@Controller、@RestController@ControllerAdvice- 过滤器、拦截器、
Converter、HandlerMethodArgumentResolver
普通 @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 层直接「模拟」请求和响应。速度快,毫秒级完成。
NoteSpring 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());
}
注意 contentType 和 content 配套:声明发的是 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 章有总览),旧写法仍可用。