本节目标:掌握切片测试的思路与三种常用切片,理解 4.x 下 @WebMvcTest 必须显式加 @AutoConfigureMockMvc、用 @MockitoBean 取代旧注解,并会用 MockMvc 与新增的 RestTestClient 写 Controller 测试。
适用版本:Spring Boot 4.1.x(Java 21)
17.2 切片测试
17.1 的单元测试把 Service 的逻辑测干净了,但它有一个前提:替身是我们自己造的。真实运行时,BookController 收到的请求要经过 Spring MVC 的映射、参数绑定、消息转换器,BookRepository 的派生查询要真的翻译成 SQL。这些「框架帮你做的事」单元测试一概覆盖不到。
一种选择是直接上 @SpringBootTest 启动完整容器,但那太贵了。切片测试就是中间路线:只加载被测那一层相关的自动配置,其余全部不装。
17.2.1 切片测试的意义
@SpringBootTest 会加载整个应用上下文;而 @WebMvcTest 只加载 MVC 相关的那一小撮自动配置,@DataJpaTest 只加载 JPA 相关的那一小撮。代价对比:
| 测试方式 | 加载内容 | 单次耗时量级 | 适用对象 |
|---|---|---|---|
| 单元测试 | 无容器 | 毫秒 | Service 纯逻辑 |
@WebMvcTest | 仅 MVC 层 | 百毫秒 | Controller、参数绑定、JSON |
@DataJpaTest | 仅 JPA + 数据源 | 百毫秒~秒 | Repository 查询、实体映射 |
@SpringBootTest | 完整上下文 | 秒级 | 端到端、多组件协作 |
切片测试的名字很形象:把完整的应用「切」下一片来测。它加载的 Bean 少,启动自然快,失败也更容易定位到具体那一层。
17.2.2 @WebMvcTest 测 Controller
先用它测 17.1 里的图书接口。要点是三个注解:
package com.example.library.web;
import static org.mockito.BDDMockito.given;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.Test;
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.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;
import com.example.library.domain.Book;
import com.example.library.service.BookService;
@WebMvcTest(BookController.class)
@AutoConfigureMockMvc
class BookControllerTest {
@Autowired
private MockMvc mockMvc;
@MockitoBean
private BookService bookService;
@Test
void getBook_existingId_returnsJson() throws Exception {
given(bookService.getById(1L))
.willReturn(new Book(1L, "Effective Java", "Joshua Bloch", 2018));
mockMvc.perform(get("/api/books/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.title").value("Effective Java"))
.andDo(print());
}
}
@WebMvcTest(BookController.class):只加载 MVC 切片,并只注册指定的 Controller。不写参数时会加载全部 Controller。@AutoConfigureMockMvc:4.x 必须显式加。3.x 里@WebMvcTest会顺带把 MockMvc 配好,4.x 取消了这层隐含,不加它@Autowired MockMvc会注入失败。@MockitoBean:把BookService换成替身。Controller 依赖的 Service 在这一层不会被真正加载,必须由你提供替身。
注意 @WebMvcTest 不会加载 BookService、BookRepository、DataSource。如果 Controller 直接注入了这些,切片启动就会失败——这正是「只测一层」的约束在提醒你分层是否正确。
17.2.3 3.x → 4.x 测试写法迁移对照表
4.x 在测试 API 上做了一批破坏性调整,把这张表记牢,能省掉大量「明明照旧教程写却起不来」的排查时间:
| 3.x 写法 | 4.x 写法 | 说明 |
|---|---|---|
@MockBean | @MockitoBean | @MockBean 已在 4.x 移除 |
@SpyBean | @MockitoSpyBean | @SpyBean 已在 4.x 移除 |
@SpringBootTest / @WebMvcTest 自带 MockMvc | 需另加 @AutoConfigureMockMvc | 取消隐含配置 |
@SpringBootTest 自带 TestRestTemplate | 需另加 @AutoConfigureTestRestTemplate | 见 17.3 |
MockitoTestExecutionListener | Mockito 的 MockitoExtension | 旧监听器已移除 |
| — | RestTestClient + @AutoConfigureRestTestClient | 4.x 新增 |
关于 @MockitoBean / @MockitoSpyBean 还有一条硬约束:它们只能用在测试类上,不能用在 @Configuration 类里。3.x 时代把 @MockBean 放进 @TestConfiguration 的写法在 4.x 会直接报错。需要覆盖 Bean 时改用 17.3 介绍的 @TestBean。
17.2.4 MockMvc 的用法
MockMvc 不启动真实 HTTP 服务器,而是直接调用 DispatcherServlet,因此快,但也因此不会经过网络层(CORS、真实端口、过滤器链的容器级部分不在此覆盖)。
一次请求固定写成三段的链式调用:
| 方法 | 作用 | 常用写法 |
|---|---|---|
perform(...) | 发起请求 | perform(get("/api/books/1")) |
andExpect(...) | 断言响应 | status().isOk()、jsonPath("$.title").value("...") |
andDo(print()) | 打印请求/响应,排查用 | andDo(print()) |
常用请求构造器(静态导入 MockMvcRequestBuilders):get / post / put / delete / patch。带 JSON 体的 POST:
mockMvc.perform(post("/api/books")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"title\":\"Effective Java\",\"author\":\"Joshua Bloch\",\"publishedYear\":2018}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.title").value("Effective Java"));
断言 JSON 用 jsonPath:$.title 取字段、$.items[0].id 取数组元素、$ 取根。校验失败场景则断言状态码与错误体字段:
mockMvc.perform(post("/api/books").contentType(MediaType.APPLICATION_JSON).content("{}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").exists());
andDo(print()) 是排障利器:断言失败时,它会把你实际发出去的请求和收到的响应完整打印出来,省去反复加断点。
17.2.5 @DataJpaTest 测 Repository
Controller 之外,另一片要测的是 Repository 的派生查询是否真能翻译成正确的 SQL。
package com.example.library.repository;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import com.example.library.domain.Book;
@DataJpaTest
class BookRepositoryTest {
@Autowired
private BookRepository bookRepository;
@Test
void findByTitle_returnsMatch() {
bookRepository.save(new Book("Effective Java", "Joshua Bloch", 2018));
assertThat(bookRepository.findByTitle("Effective Java")).isPresent();
}
}
@DataJpaTest 有三条关键默认行为:
- 事务回滚:每个测试方法跑在一个事务里,结束后回滚,测试之间互不污染。
- 替换数据源:默认把应用配置的数据源替换成内嵌数据库(如 H2),所以它不需要真实数据库就能跑。
- 只装配 JPA:加载
@Entity、Repository、TestEntityManager,不加载 Controller、不加载 Service。
想让它连真实数据库(或 Testcontainers 提供的数据库)而不是内嵌库,用:
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BookRepositoryRealDbTest {
// 此时使用 application.yml / Testcontainers 里的真实数据源
}
@DataJpaTest 还自动提供一个 TestEntityManager,用于把实体刷进库而不经过 Repository,方便准备数据。
17.2.6 @JsonTest 测序列化
如果序列化逻辑(字段名、日期格式、自定义序列化器)本身值得验证,用 @JsonTest。它只装配 Jackson,是范围最小的切片。4.x 用 Jackson 3,自动配置的是 JsonMapper(包名 tools.jackson.databind.json):
package com.example.library.json;
import static org.assertj.core.api.Assertions.assertThat;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import com.example.library.domain.Book;
import tools.jackson.databind.json.JsonMapper;
@JsonTest
class BookJsonTest {
@Autowired
private JsonMapper jsonMapper;
@Test
void serialize_book_containsTitle() {
Book book = new Book(1L, "Effective Java", "Joshua Bloch", 2018);
String json = jsonMapper.writeValueAsString(book);
assertThat(json).contains("\"title\":\"Effective Java\"");
}
}
注意 JsonMapper 的导入路径是 tools.jackson.databind.json.JsonMapper——这是 4.x 采用 Jackson 3 的直接体现,写 3.x 教程里的 com.fasterxml.jackson.databind.ObjectMapper 在这里已经不适用。
17.2.7 切片到底加载了什么
把三种切片的边界列成一张表,能帮你判断某个测试该用哪种:
| 切片 | 加载 | 不加载 |
|---|---|---|
@WebMvcTest | MVC、Jackson、@ControllerAdvice、Filter、Validator、MockMvc | Service、Repository、DataSource |
@DataJpaTest | JPA、DataSource、实体、Repository、Flyway/Liquibase(如引入) | MVC、Service、Controller |
@JsonTest | 仅 Jackson(JsonMapper) | 其余全部 |
判断方法很简单:你被测的对象依赖谁,就用能加载谁的切片,并给剩下的一层加 @MockitoBean。 Controller 依赖 Service,就用 @WebMvcTest 并把 Service 换成替身;Repository 依赖 DataSource,就用 @DataJpaTest。
17.2.8 4.x 新增:RestTestClient
MockMvc 的链式断言是 Spring MVC 特有的。4.x 引入了一个更通用的客户端 RestTestClient,它配合 @AutoConfigureRestTestClient 使用,既可以测 @WebMvcTest 下的 MVC 接口,也可以在集成测试里用同一套 API 发真实请求:
package com.example.library.web;
import static org.mockito.BDDMockito.given;
import org.junit.jupiter.api.Test;
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.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.client.RestTestClient;
import com.example.library.domain.Book;
import com.example.library.service.BookService;
@WebMvcTest(BookController.class)
@AutoConfigureRestTestClient
class BookControllerRestTestClientTest {
@Autowired
private RestTestClient restTestClient;
@MockitoBean
private BookService bookService;
@Test
void getBook() {
given(bookService.getById(1L))
.willReturn(new Book(1L, "Effective Java", "Joshua Bloch", 2018));
restTestClient.get().uri("/api/books/1")
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.title").isEqualTo("Effective Java");
}
}
与 MockMvc 的差别只在表达方式:perform 换成 get().uri(...).exchange(),andExpect 换成 expectStatus() / expectBody()。RestTestClient 的价值在于同一套客户端 API 能横跨切片测试与集成测试,不必在两种风格之间切换。本节里它和 MockMvc 二选一即可;17.3 会看到它在 @SpringBootTest 下的用法。
小结
- 切片测试只加载被测那一层相关的自动配置,比
@SpringBootTest快、定位准。 @WebMvcTest测 Controller,4.x 必须显式加@AutoConfigureMockMvc,否则 MockMvc 注入失败。- 用
@MockitoBean/@MockitoSpyBean取代已移除的旧注解;它们不能用在@Configuration类里。 - 迁移对照:旧注解已移除、MockMvc 与 TestRestTemplate 需显式开启、
MockitoTestExecutionListener换成MockitoExtension、新增RestTestClient。 MockMvc三段式:perform发请求、andExpect断言、andDo(print())排查;jsonPath断言响应字段。@DataJpaTest默认事务回滚、替换为内嵌数据库、只装配 JPA;要连真实库用@AutoConfigureTestDatabase(replace = NONE)。@JsonTest只装配 Jackson 3 的JsonMapper(tools.jackson.databind.json),用于验证序列化本身。- 4.x 新增
RestTestClient+@AutoConfigureRestTestClient,一套 API 横跨切片与集成测试。
切片测试各自守住一层,但真实系统里 Controller、Service、Repository 是串起来工作的,跨层协作、事务、真实数据库方言都还没验证。下一节我们用 @SpringBootTest 做集成测试,把整条链路打通。
阅读导航:上一节:17.1 单元测试 · 下一节:17.3 集成测试 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。