Spring Test与综合实战详解
定位:讲透 Spring 测试体系(上下文管理、事务回滚、Mock 替换)、分层测试策略,并汇总 Framework 层高频踩坑
适用版本:Spring Framework 6.x(JDK 17+)
目录
一、测试体系定位
1.1 Spring Test 提供的四个核心能力
| 能力 | 说明 |
|---|---|
| 测试上下文管理与缓存 | 相同配置的 ApplicationContext 在测试套件内缓存复用,避免每个测试重启容器 |
| 依赖注入 | 测试类本身被注入容器中的 Bean |
| 事务管理 | 测试类标 @Transactional,每个测试方法执行后自动回滚,不脏库 |
| Mock 集成 | @MockBean/@SpyBean 替换容器中的 Bean,与 Mockito 协同 |
1.2 测试金字塔中的位置
完整集成测试(少):真容器 + 真实依赖
切片测试(适量):只加载相关层的上下文
单元测试(大量):不加载容器,纯对象 + Mockito
原则:容器是昂贵资源,能不起就不起。大量逻辑应该在无容器的单元测试中覆盖,Spring 上下文留给"需要验证装配与集成"的场景。
二、核心注解族
2.1 上下文加载
java
// 纯 Framework 测试入口(JUnit 5)
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
class OrderServiceTest {
@Autowired private OrderService orderService;
}
Boot 项目中 @SpringBootTest 一站式完成(BT 篇深讲);纯 Framework 项目用 SpringExtension + @ContextConfiguration。
2.2 事务回滚
java
@Transactional // 测试类或方法上
class DaoIntegrationTest {
@Test void insert() { dao.save(...); /* 断言 */ }
// 测试结束自动回滚,数据库无残留
}
注意:回滚意味着"提交后行为"测不到 (如依赖提交触发的逻辑);需要真实提交时用 @Commit / @Rollback(false)。
2.3 Mock 替换
| 注解 | 作用 |
|---|---|
| @MockBean | 用 Mockito mock 替换容器中的 Bean(整层打桩,如 mock 掉外部客户端) |
| @SpyBean | 真实对象包一层,可选择性打桩(部分 mock) |
2.4 @DirtiesContext
替换/污染了容器状态后标记"上下文已脏",强制下一个测试重建容器。代价大(重启容器),慎用------多数场景 @MockBean 已隔离影响,不需要脏化整个上下文。
三、分层测试策略
| 层 | 策略 | 工具 |
|---|---|---|
| 领域/Service 逻辑 | 纯单元测试:new 服务 + mock 依赖 | JUnit + Mockito,不起容器 |
| Web 层 | 切片测试:只加载 Web 层,模拟请求 | @WebMvcTest + MockMvc(WB 篇) |
| 数据层 | 切片测试:只加载数据层 + 内嵌库 | @DataJpaTest / H2(SD 篇) |
| 关键业务链路 | 完整集成测试(少量) | @SpringBootTest + Testcontainers |
| 定时/异步任务 | 抽出纯逻辑单测 + 容器层少量验证 | --- |
两个纪律:
- 可测试性反哺设计:构造器注入的类天然可单测;到处静态调用/字段注入的代码只能靠容器测------测试困难是设计问题的信号;
- 外部依赖的集成测试用 Testcontainers(真实 MySQL/Redis 容器),内嵌库(H2)只适合轻量场景,方言差异会骗人。
四、综合踩坑(Framework 层汇总)
本篇汇总前七篇的坑形成排查地图(机制详解见对应篇):
4.1 容器与启动
| 现象 | 指向 |
|---|---|
| 启动循环依赖报错 | 02 篇:构造器环/prototype 环/@Async 冲突 |
| 启动慢 | 组件扫描范围过大、重 Bean 未 @Lazy、条件装配类过多 |
| Bean 没注册上 | 05 篇:条件装配顺序、扫描路径、@Profile 未激活 |
4.2 代理相关
| 现象 | 指向 |
|---|---|
| 事务/缓存/自研切面不生效 | 03/06 篇:自调用、非 public、异常吞 |
| 注入具体类启动报错 | 03 篇:JDK 代理只实现接口;统一 CGLIB 或面向接口 |
| @Async 同步执行 | 07 篇:自调用/未开启 |
4.3 生命周期与测试
| 现象 | 指向 |
|---|---|
| @PostConstruct 里依赖的 Bean 行为异常 | 01 篇:初始化顺序≠依赖就绪语义,改用事件或延迟 |
| 测试慢 | @DirtiesContext 滥用、每个测试重启容器 |
| 测试数据互相污染 | 未用事务回滚或 @MockBean 隔离 |
五、总结
- Spring Test 四能力:上下文缓存复用、测试类注入、事务自动回滚、@MockBean/@SpyBean 替换;上下文昂贵,能不起就不起。
- 注解族:SpringExtension + @ContextConfiguration(纯 Framework 入口);@Transactional 测试回滚;@DirtiesContext 慎用。
- 分层策略:领域逻辑纯单测、Web/数据层切片、关键链路少量完整集成;测试困难是设计信号(构造器注入反哺可测性)。
- 踩坑地图:容器类(循环依赖/条件装配)、代理类(自调用/类型)、生命周期类(初始化时机/测试污染)------每类都可定位到对应机制篇。
六、常见高频面试题
1. Spring 测试里为什么要缓存 ApplicationContext?
要点:启动完整容器昂贵(扫描、实例化所有单例)。Spring Test 按"上下文配置指纹"缓存已创建的上下文,相同配置的多个测试类/方法共享复用,大幅加快测试套件。副作用:上下文状态被共享,测试间可能互相污染------破坏状态的测试用 @DirtiesContext 强制重建(代价高),更优的是用 @MockBean 隔离或事务回滚。
2. 测试类上的 @Transactional 有什么作用?
要点:让每个测试方法在事务中执行,方法结束后自动回滚,数据库不留测试数据,测试可重复可并行。注意两点:回滚导致"提交后行为"(如依赖 AFTER_COMMIT 的逻辑)测不到,需要时用 @Commit;异步/新线程中的操作不在该事务内,回滚管不到。
3. @MockBean 和 Mockito 的 @Mock 有什么区别?
要点:作用域不同。@Mock 是纯 Mockito,创建一个假对象,手工传参给被测类,不涉及容器;@MockBean 是 Spring Test 的,把 mock 注册进 ApplicationContext 替换(或补充)容器中的真实 Bean,影响所有注入它的对象。切片/集成测试用 @MockBean 打桩外部依赖(如第三方客户端);纯单元测试用 @Mock + 构造器注入,不起容器更快。
4. 单元测试和集成测试怎么划分?
要点:按"是否验证容器装配"划分。单元测试:不加载 Spring 容器,new 被测类 + Mockito mock 依赖,覆盖业务逻辑与边界,数量最大、速度最快;集成测试:加载(完整或切片)容器,验证装配、事务、数据访问、Web 链路等集成行为,数量少而精。划分原则:逻辑错误靠单测抓,装配与集成错误靠集成测试抓;测试困难往往说明设计耦合(如静态依赖)。
5. @DirtiesContext 什么时候用?为什么说慎用?
要点:当测试修改了容器级状态(替换了 Bean 行为、改了配置属性、污染了单例)且会影响后续测试时,标记上下文已脏让框架重建。慎用的原因:重建上下文等于重启容器,测试套件显著变慢。多数场景有更轻的替代:@MockBean 隔离替换、事务回滚清理数据、或将该测试独立成类单独配置上下文。
6. 为什么说"测试困难是设计问题的信号"?
要点:难以单测的代码通常有结构问题:字段注入无法脱离容器构造、静态方法调用无法替换、类职责过大依赖过多。Spring 推崇构造器注入正是因为它让 new Service(mockA, mockB) 直接可行。改进方向:依赖显式化(构造器)、副作用外置(接口抽象)、职责拆分。可测试性与可维护性同源------好设计的代码天然好测。
7. 如何测试 @Scheduled 定时任务和 @Async 异步方法?
要点:核心逻辑与触发机制分离。把定时/异步方法内部的业务逻辑抽成普通方法,对其做充分单元测试(快速、可覆盖边界);触发层(调度是否执行、异步是否提交)用少量集成测试或容器测试验证。@Async 测试注意上下文传播问题不在此暴露;也可在测试配置中把异步执行器换成同步实现(SyncTaskExecutor),让异步逻辑可确定性断言。
8. 集成测试中外部依赖(MySQL/Redis)怎么处理?
要点:三种方案按保真度递增。① 内嵌库(H2):零依赖最快,但方言与行为差异会骗人,只适合轻量;② Testcontainers:启动真实数据库/中间件容器,保真度高,是集成测试首选;③ 专用测试环境:共享测试库,数据隔离成本高。纪律:关键数据访问路径用真实引擎验证,避免"内嵌库测过、线上方言炸"。
9. Spring 测试启动很慢,怎么优化?
要点:① 减少上下文重启:避免滥用 @DirtiesContext,相同配置共享缓存;② 用切片测试替代完整上下文(@WebMvcTest/@DataJpaTest 只加载需要的层);③ 缩小组件扫描范围、重资源 Bean 测试配置中 @Lazy;④ 并行测试套件(上下文缓存配合并行);⑤ 能纯单测的逻辑移出容器测试。目标是把"需要容器的测试"数量压到最少。
10. 测试里怎么替换配置属性(如切换测试数据源)?
要点:常用手段:测试资源目录的 application-{test}.yml 配 @ActiveProfiles("test") 激活;@TestPropertySource 直接注入属性(优先于配置文件);@SpringBootTest(properties = {...}) 行内属性;动态值用 ApplicationContextInitializer 或 @DynamicPropertySource(Testcontainers 场景标准姿势:把容器的随机端口注入配置)。优先级遵循属性源规则(FW-04):测试注入的源覆盖配置文件。
