《Spring Boot 入门》17.2 切片测试

切片测试只加载需要的那一层,比集成测试快得多。本节用 @WebMvcTest 测 Controller、@DataJpaTest 测 Repository、@JsonTest 测序列化,讲清 4.x 必须显式加 @AutoConfigureMockMvc、用 @MockitoBean 取代旧注解,并给出 3.x 到 4.x 的迁移对照表与新增的 RestTestClient 用法。

本节目标:掌握切片测试的思路与三种常用切片,理解 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
MockitoTestExecutionListenerMockito 的 MockitoExtension旧监听器已移除
—RestTestClient + @AutoConfigureRestTestClient4.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 有三条关键默认行为:

  1. 事务回滚:每个测试方法跑在一个事务里,结束后回滚,测试之间互不污染。
  2. 替换数据源:默认把应用配置的数据源替换成内嵌数据库(如 H2),所以它不需要真实数据库就能跑。
  3. 只装配 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 切片到底加载了什么

把三种切片的边界列成一张表,能帮你判断某个测试该用哪种:

切片加载不加载
@WebMvcTestMVC、Jackson、@ControllerAdvice、Filter、Validator、MockMvcService、Repository、DataSource
@DataJpaTestJPA、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 集成测试 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计