你真的需要每个测试都把整个 Spring Boot 应用启动一遍吗?

举报
bug菌 发表于 2026/01/13 16:06:04 2026/01/13
【摘要】 🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。  本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。...

🏆本文收录于《滚雪球学SpringBoot 3》:https://blog.csdn.net/weixin_43970743/category_12795608.html,专门攻坚指数提升,本年度国内最系统+最专业+最详细(永久更新)。
  
本专栏致力打造最硬核 SpringBoot3 从零基础到进阶系列学习内容,🚀均为全网独家首发,打造精品专栏,专栏持续更新中…欢迎大家订阅持续学习。 如果想快速定位学习,可以看这篇【SpringBoot3教程导航帖】https://blog.csdn.net/weixin_43970743/article/details/151115907,你想学习的都被收集在内,快速投入学习!!两不误。
  
若还想学习更多,可直接前往《滚雪球学SpringBoot(全版本合集)》:https://blog.csdn.net/weixin_43970743/category_11599389.html,涵盖SpringBoot所有版本教学文章。

演示环境说明:

  • 开发工具:IDEA 2021.3
  • JDK版本: JDK 17(推荐使用 JDK 17 或更高版本,因为 Spring Boot 3.x 系列要求 Java 17,Spring Boot 3.5.4 基于 Spring Framework 6.x 和 Jakarta EE 9,它们都要求至少 JDK 17。)
  • Spring Boot版本:3.5.4(于25年7月24日发布)
  • Maven版本:3.8.2 (或更高)
  • Gradle:(如果使用 Gradle 构建工具的话):推荐使用 Gradle 7.5 或更高版本,确保与 JDK 17 兼容。
  • 操作系统:Windows 11

反问一句:**你希望上线后才知道“对方改了字段名”,还是在 PR 阶段就让测试把它按在地上摩擦?**🙂

前言

反问一句:**你是想测功能,还是想测耐心?**🙂

0. 先把“官方定义”摆在桌面上

Spring Boot 官方文档明确提到:除了 @SpringBootTest 之外,还提供了许多注解用于测试应用程序的“更具体切片”。也就是说,切片测试是官方路线,不是什么民间偏方。

同时,Spring Boot 还专门列了一个“Test Slices”清单,告诉你各种 @…Test 注解默认导入哪些自动配置。

这两句的潜台词很直白:

  • @SpringBootTest:你需要“整机联调”,那就上。
  • Test Slices:你只想验证某一层的行为,就别把全家桶端出来。

1) @SpringBootTest 这种“大而全”测试的性能瓶颈

1.1 它到底“全”在哪里?

官方说得很客气:@SpringBootTest 会通过 SpringApplication 来创建测试用的 ApplicationContext,以便启用 Spring Boot 的各种特性。

翻译成人话:

  • 会走自动配置(Auto-Configuration):你项目里引了啥 starter,它就可能配啥。
  • 会做组件扫描:配置类、Bean 定义、各种 @Configuration 都可能进来。
  • 可能还会准备 Web 环境(取决于 webEnvironment)。
  • 上下文启动成本高:类多、Bean 多、条件装配多,启动就慢。

你会发现瓶颈通常不在“跑测试方法”,而在——加载上下文

1.2 你以为你在测业务,其实你在测“启动速度”

我见过最经典的尴尬场景:

  • 你写了 200 个测试类
  • 每个类都 @SpringBootTest
  • 其中 180 个其实只是测一些简单逻辑、或者测 Controller 的参数校验
    最后 CI 直接变成“上下文加载马拉松”。

更要命的是:上下文还可能被你自己“整碎”。比如:

  • 测试类上加了一堆 @TestPropertySource 或 profiles,导致上下文无法复用。
  • 你动不动 @DirtiesContext,上下文缓存直接失效,等于每次重启应用。
  • 你的 Auto-Config 里顺手连外部依赖(比如某个定制 starter 启动时就去探测远程服务),CI 环境一抖就崩。

1.3 什么时候它“必须上”?

我对 @SpringBootTest 的态度其实很中立:该用就用。比如:

  • 你要测多个层的真实协作:Controller → Service → Repository → DB
  • 你要验证条件装配、Bean 覆盖、配置属性绑定是否正确
  • 你要做端到端集成测试(甚至随机端口跑 HTTP 调用)

但——如果你只是测 MVC 映射、JPA 查询、JSON 序列化,这种“局部行为”,那你上 @SpringBootTest 就像:为了削一根铅笔,把木工车间整个开起来。帅是帅,划不来。


2) @WebMvcTest:只加载 Controller 层组件(以及它的“最常见坑”)

2.1 它到底加载什么?

@WebMvcTest 的官方 API 文档写得很硬核:它会限制组件扫描范围,主要聚焦在 Web 层相关 Bean(例如 @Controller@ControllerAdvice@JsonComponent、各种 Converter、Filter、HttpMessageConverter 等),并且默认会自动配置 Spring Security 与 MockMvc

这句话信息量很大:

  • 你能直接注入 MockMvc 做请求级别测试
  • 你不会把 Service/Repository 自动带进来
  • 你如果用了 Spring Security,它还会帮你把安全过滤器链也配出来(这点既是优点,也是“坑源”)

2.2 一个很“像人类”的误解:我以为它会把 Service 也注入

不会。它不会。它真的不会。🙂
@WebMvcTest 的哲学是:你测 Controller,就别把 Service 也拉来陪跑
所以你有两种常见做法:

  1. @MockBean 把依赖的 Service mock 掉(最常用)
  2. 或者用 @Import 显式引入你想带进来的少量配置(谨慎)

2.3 实战代码:用 @WebMvcTest 测参数校验、异常处理与返回 JSON

假设我们有一个 Controller:

@RestController
@RequestMapping("/api/books")
class BookController {

    private final BookService bookService;

    BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @PostMapping
    public BookDto create(@Valid @RequestBody CreateBookCmd cmd) {
        return bookService.create(cmd);
    }

    @GetMapping("/{id}")
    public BookDto get(@PathVariable Long id) {
        return bookService.get(id);
    }
}

record CreateBookCmd(
        @NotBlank(message = "title 不能为空")
        @Size(max = 80, message = "title 太长了")
        String title
) {}

record BookDto(Long id, String title) {}

然后我们写测试:

@WebMvcTest(BookController.class)
class BookControllerWebMvcTest {

    @Autowired MockMvc mockMvc;

    @MockBean BookService bookService;

    @Test
    void should_return_400_when_title_blank() throws Exception {
        mockMvc.perform(post("/api/books")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {"title": "   "}
                                """))
                .andExpect(status().isBadRequest())
                .andExpect(content().string(org.hamcrest.Matchers.containsString("title 不能为空")));
    }

    @Test
    void should_create_book() throws Exception {
        Mockito.when(bookService.create(Mockito.any()))
                .thenReturn(new BookDto(1L, "Spring in Action"));

        mockMvc.perform(post("/api/books")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content("""
                                {"title": "Spring in Action"}
                                """))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.title").value("Spring in Action"));
    }
}

你看这个测试的“爽点”在哪里?

  • 不启动服务器
  • 不连数据库
  • 直接验证:路由映射 + JSON 反序列化 + 参数校验 + 响应序列化
  • 速度通常比 @SpringBootTest 轻快一大截

2.4 @WebMvcTest 的两个高频“翻车点”

翻车点 A:Spring Security 把你挡在门外
因为它默认会自动配置 Spring Security。
于是你可能写了个“最普通的 GET”,结果返回 401/403,心态瞬间爆炸:

“我只是想测 Controller,你为什么要考我安全?”

解决思路(常用三选一):

  • 在测试里使用 @WithMockUser
  • 或者配置 SecurityFilterChain 的测试替身(测试配置类)
  • 或者用 @AutoConfigureMockMvc(addFilters = false) 暂时关过滤器(适合非安全相关测试,但别滥用)

翻车点 B:Controller 依赖的 Bean 没进上下文
因为它只加载 Web 层,Service 不会自动出现。
所以你要用 @MockBean 把缺的依赖补上。

3) @DataJpaTest / @DataRedisTest:专注持久层测试(别拿它们去测 Service)

3.1 @DataJpaTest:JPA 组件专用“切片”

官方 API 文档说得很明确:

  • 只启用与 Data JPA 测试相关的自动配置
  • 组件扫描限制在 JPA repositories 与 @Entity 实体
  • 默认是事务性的,并且每个测试结束会回滚
  • 默认会用嵌入式内存数据库替换 DataSource(可用 @AutoConfigureTestDatabase 改)

我特别喜欢它的两个默认行为:

  1. 事务 + 自动回滚:你不用自己 cleanup 数据,测试之间天然隔离。
  2. 只测持久层:Repository 查询写错了,它能很快、很准地揪出来。

3.2 实战代码:测一个“看似简单但很容易写错”的查询

假设你有实体与仓库:

@Entity
class Book {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, length = 80)
    private String title;

    protected Book() {}

    public Book(String title) { this.title = title; }

    public Long getId() { return id; }
    public String getTitle() { return title; }
}

interface BookRepository extends JpaRepository<Book, Long> {
    List<Book> findByTitleContainingIgnoreCase(String keyword);
}

测试:

@DataJpaTest
class BookRepositoryDataJpaTest {

    @Autowired BookRepository bookRepository;
    @Autowired TestEntityManager em;

    @Test
    void should_find_books_by_title_keyword_ignore_case() {
        em.persist(new Book("Spring Boot Guide"));
        em.persist(new Book("Kotlin for Spring"));
        em.persist(new Book("Distributed Systems"));
        em.flush();

        var result = bookRepository.findByTitleContainingIgnoreCase("spring");

        org.assertj.core.api.Assertions.assertThat(result)
                .extracting(Book::getTitle)
                .containsExactlyInAnyOrder("Spring Boot Guide", "Kotlin for Spring");
    }
}

这里的关键不是“能跑”,而是:

  • 用真实 JPA 行为验证方法命名派生查询是否符合预期
  • 避免你在 Service 层用假数据自嗨(很多 bug 就是这么自嗨出来的)

3.3 @DataRedisTest:Redis 组件专用“切片”

官方 API 文档同样清晰:

  • 只启用与 Data Redis 测试相关的自动配置
  • 组件扫描限制在 Redis repositories 与 @RedisHash 实体

**它的定位很明确:**你测的是 Redis 映射、Repository 行为、序列化策略、TTL 等持久层相关内容,而不是测 Controller 或业务编排。

一个现实提醒(别装):Redis 测试最容易被“环境”坑

JPA 默认能给你内存数据库兜底,但 Redis 没那么统一。很多团队会用:

  • Testcontainers 拉一个真实 Redis
  • 或者在 CI 里起 Redis 服务
    这不是切片本身的问题,是 Redis 的性质决定的:你最好给它一个真实环境,否则你测出来的东西可能“像 Redis”,但不一定“是 Redis”。(这句话我说得已经很克制了😅)

3.4 实战代码:用 @DataRedisTest@RedisHash 与 Repository

@RedisHash("book")
class BookCache {
    @Id
    private String id;
    private String title;

    protected BookCache() {}

    BookCache(String id, String title) {
        this.id = id;
        this.title = title;
    }

    public String getId() { return id; }
    public String getTitle() { return title; }
}

interface BookCacheRepository extends CrudRepository<BookCache, String> {}

测试:

@DataRedisTest
class BookCacheDataRedisTest {

    @Autowired BookCacheRepository repo;

    @Test
    void should_save_and_find_book_cache() {
        repo.save(new BookCache("1", "Spring Boot Guide"));

        var found = repo.findById("1");

        org.assertj.core.api.Assertions.assertThat(found)
                .isPresent()
                .get()
                .extracting(BookCache::getTitle)
                .isEqualTo("Spring Boot Guide");
    }
}

如果你跑不起来,十有八九不是你代码写错,而是 Redis 环境没准备好——这时候别急着怀疑人生,先怀疑基础设施(这很“工程师”,也很“人类”🙂)。

4) @JsonTest:测试 JSON 序列化与反序列化(这玩意儿真的值)

4.1 它“切”出来的到底是什么?

官方 API 文档写得很直:

  • @JsonTest 只启用与 JSON 测试相关的自动配置
  • 组件扫描限制在 @JacksonComponent(以及 Jackson Module 等)
  • 默认会初始化 JacksonTester / JsonbTester / GsonTester 字段(可用 @AutoConfigureJsonTesters 更细控制)

你看,这就是典型的“我只想测 JSON,其他都别来烦我”。

4.2 为什么我强烈建议你写 @JsonTest

因为 JSON 出 bug 的方式非常阴险:

  • 字段名大小写、下划线、别名映射
  • LocalDateTime 格式
  • 忽略字段(@JsonIgnore
  • 枚举序列化策略
  • null 到底要不要输出
    这些东西在 Controller 测试里也能覆盖一部分,但 Controller 测试太“综合”,一旦失败你很难第一时间定位到底是 MVC 绑定问题、还是 JSON 映射问题。

@JsonTest 的价值就是:**把变量收敛到只有 JSON。**失败了就别狡辩,基本就是序列化/反序列化本身的问题。

4.3 实战代码:用 JacksonTester 验证序列化输出

DTO:

class PaymentResponse {
    @com.fasterxml.jackson.annotation.JsonIgnore
    private String internalId;

    @com.fasterxml.jackson.annotation.JsonProperty("payment_amount")
    private java.math.BigDecimal amount;

    @com.fasterxml.jackson.annotation.JsonFormat(shape = com.fasterxml.jackson.annotation.JsonFormat.Shape.STRING,
            pattern = "yyyy-MM-dd'T'HH:mm:ss")
    private java.time.LocalDateTime paidAt;

    // getters/setters 省略(别嫌我偷懒,我怕你嫌我太啰嗦🙂)
}

测试:

@JsonTest
class PaymentResponseJsonTest {

    @Autowired private JacksonTester<PaymentResponse> json;

    @Test
    void should_serialize_payment_response() throws Exception {
        var dto = new PaymentResponse();
        // dto.setInternalId("SECRET-42");
        dto.setAmount(new java.math.BigDecimal("42.50"));
        dto.setPaidAt(java.time.LocalDateTime.of(2026, 1, 12, 10, 30, 0));

        var content = json.write(dto);

        org.assertj.core.api.Assertions.assertThat(content).doesNotHaveJsonPath("$.internalId");
        org.assertj.core.api.Assertions.assertThat(content).extractingJsonPathNumberValue("$.payment_amount")
                .isEqualTo(42.50);
        org.assertj.core.api.Assertions.assertThat(content).extractingJsonPathStringValue("$.paidAt")
                .isEqualTo("2026-01-12T10:30:00");
    }
}

这段测试的观感很“干净”:

  • 不关心 Spring MVC
  • 不关心数据库
  • 不关心应用上下文里有什么 Service
    只验证:对象 ↔ JSON 的契约有没有被你写崩。

5) 选型心法:什么时候用切片,什么时候回到 @SpringBootTest

我给你一个非常“上班族真实”的判断标准(没那么学术,但好用):

5.1 你关心的是“边界契约”,优先切片

  • Controller 的路由、参数绑定、校验、异常映射 → @WebMvcTest
  • Repository 的查询、映射、事务回滚 → @DataJpaTest
  • Redis Repository / @RedisHash 映射 → @DataRedisTest
  • JSON 序列化反序列化 → @JsonTest

5.2 你关心的是“跨层协作”,再上 @SpringBootTest

比如:

  • 下单:Controller 收请求 → Service 编排 → Repository 写库 → 发消息
    这种你切片切不出来真实信心,就该 @SpringBootTest 上场。

5.3 切片测试不是“降级”,它是“更精准”

很多人潜意识觉得:

“集成测试更高级,切片测试像小儿科。”

我每次听到都想笑:精准才高级
你把问题空间缩小,失败定位更快,反馈更短,CI 更稳定——这才是工程的正道。

6) 最后一点“带情绪但不偏激”的真话:别把测试写成自我感动

测试的目的从来不是“让覆盖率好看”,而是:

  • 让你改代码时没那么心虚
  • 让你上线前睡得踏实一点
  • 让你出事故时能迅速缩小嫌疑范围

@SpringBootTest 很好,但它不是万金油;Test Slices 很香,但它也不是万能。真正成熟的姿势是:

  • 大量切片测试覆盖“边界契约”
  • 少量 @SpringBootTest 覆盖“跨层协作”
  • 再配上少量端到端测试兜底“真实链路”

所以我也想再反问你一句(别嫌我啰嗦😅):

你写测试,是想证明“我写过测试”,还是想在未来某天救自己一命?

如果是后者——欢迎来到切片测试的世界,它不花哨,但真的管用。

🧧福利赠与你🧧

  无论你是计算机专业的学生,还是对编程有兴趣的小伙伴,都建议直接毫无顾忌的学习此专栏「滚雪球学SpringBoot」,bug菌郑重承诺,凡是学习此专栏的同学,均能获取到所需的知识和技能,全网最快速入门SpringBoot,就像滚雪球一样,越滚越大, 无边无际,指数级提升。

最后,如果这篇文章对你有所帮助,帮忙给作者来个一键三连,关注、点赞、收藏,您的支持就是我坚持写作最大的动力。

同时欢迎大家关注公众号:「猿圈奇妙屋」 ,以便学习更多同类型的技术文章,免费白嫖最新BAT互联网公司面试题、4000G PDF编程电子书、简历模板、技术文章Markdown文档等海量资料。

ps:本文涉及所有源代码,均已上传至Gitee:https://gitee.com/bugjun01/SpringBoot-demo 开源,供同学们一对一参考 Gitee传送门https://gitee.com/bugjun01/SpringBoot-demo,同时,原创开源不易,欢迎给个star🌟,想体验下被🌟的感jio,非常感谢❗

🫵 Who am I?

我是 bug菌:

  • 热活跃于 CSDN:https://blog.csdn.net/weixin_43970743 | 掘金:https://juejin.cn/user/695333581765240 | InfoQ:https://www.infoq.cn/profile/4F581734D60B28/publish | 51CTO:https://blog.51cto.com/u_15700751 | 华为云:https://bbs.huaweicloud.com/community/usersnew/id_1582617489455371 | 阿里云:https://developer.aliyun.com/profile/uolxikq5k3gke | 腾讯云:https://cloud.tencent.com/developer/user/10216480/articles 等技术社区;
  • CSDN 博客之星 Top30、华为云多年度十佳博主&卓越贡献奖、掘金多年度人气作者 Top40;
  • 掘金、InfoQ、51CTO 等平台签约及优质作者;
  • 全网粉丝累计 30w+

更多高质量技术内容及成长资料,可查看这个合集入口 👉 点击查看:https://bbs.csdn.net/topics/612438251 👈️
硬核技术公众号 「猿圈奇妙屋」https://bbs.csdn.net/topics/612438251 期待你的加入,一起进阶、一起打怪升级。

- End -

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。