测试金字塔与 Spring Boot 定位
测试金字塔把测试分成三层:底层是大量快速、廉价的单元测试,中层是少量服务级测试,顶层是少数端到端测试。越往下越便宜、越快,越往上越接近真实环境但成本越高。
Spring Boot 的测试支持贯穿全部三层:JUnit 5 做单元测试,切片测试(Slice Test)和 MockMvc 覆盖控制器与数据层,Testcontainers 拉起真实容器跑集成测试。关键是每层测试各自聚焦,避免一测就启动整个应用。
很多项目的问题不是测试太少,而是集成测试又慢又不稳定,导致大家干脆不跑。合理分配测试层级,让大多数情况都能秒级反馈,测试才能真正跑起来。
测试的价值不在数量,而在能否在几秒内告诉你哪里坏了。
- JUnit 5单元测试基础。
- 切片测试@WebMvcTest 与 @DataJpaTest。
- MockMvc控制器接口测试。
- Testcontainers真实环境集成测试。
JUnit 5 单元测试基础
单元测试只验证一个类的逻辑,不启动 Spring 容器。Spring Boot 项目里普通工具类、计算逻辑、校验规则的测试都属于这一层,用 JUnit 5 的注解就能写得很干净。
class OrderServiceTest {
@Test
void 满减计算应正确() {
OrderService service = new OrderService();
double result = service.discount(200, 0.1);
assertThat(result).isEqualTo(180.0);
}
}
断言库建议用 AssertJ,它的链式断言读起来接近自然语言,报错信息也更友好。单元测试要覆盖分支:正常路径、边界值、异常输入,这几类用例比数量更有意义。
纯单元测试毫秒级完成,能在 CI 里每次都跑。它们是最便宜的保险,值得把核心业务逻辑的覆盖率做上去。
@WebMvcTest 与 MockMvc
控制器测试不需要启动整个应用,用 @WebMvcTest 只加载 Web 层,配合 MockMvc 模拟 HTTP 请求,直接验证接口的路径、参数、状态码和响应体。
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
MockMvc mockMvc;
@MockBean
UserService userService;
@Test
void 查询用户应返回200() throws Exception {
when(userService.find(1L)).thenReturn(new User(1L, "小码"));
mockMvc.perform(get("/api/users/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("小码"));
}
}
@MockBean 把依赖的服务替换成替身,让测试只关心控制器本身的逻辑:路由、参数绑定、状态码、响应结构。jsonPath 用来校验 JSON 响应里的具体字段。
MockMvc 不真正走网络,只在内核里模拟请求分发,速度快、可重复,是接口测试的主力。
数据层测试与 Testcontainers
数据层的 SQL 和映射逻辑用 @DataJpaTest 做切片测试,它只加载 JPA 相关配置并自动使用内存数据库。但内存数据库的行为和真实数据库有差异,敏感的 SQL 还是要在真库上验证。
Testcontainers 能在测试时用 Docker 拉起真实的 MySQL、PostgreSQL、Redis 容器,测试结束自动销毁。它让集成测试逼近生产环境,又不污染开发机。
@Testcontainers
class OrderRepositoryTest {
@Container
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16");
@Test
void 自定义查询应返回正确结果() {
// 连接 postgres.getJdbcUrl() 运行真实 SQL
}
}
Testcontainers 首次拉镜像会慢一些,之后有缓存就会快很多。它是「数据库相关 bug 上线才暴露」这类问题的有力防线。
集成测试与事务回滚
集成测试验证多个组件协同:控制器、服务、仓库、数据库一条链路。用 @SpringBootTest 启动完整上下文,但为了可重复,测试数据要隔离,事务回滚是常用手段。
@SpringBootTest
@Transactional
class OrderFlowTest {
@Autowired
OrderService orderService;
@Test
void 下单后库存应扣减() {
orderService.create(1L, 2);
// 校验库存变化
}
}
@Transactional 让每个测试方法结束后回滚,数据不会残留。注意 Testcontainers 场景下事务回滚与真实提交行为不同,涉及跨库或异步的用例要按真实提交验证。
集成测试数量要克制,挑关键业务链路写,否则整套测试会越来越慢,最后没人愿意等。
测试替身与常见坑
Mockito 的 when、verify、ArgumentCaptor 是常用的替身工具。但替身用多了,测试会变成「测试实现」而不是「测试行为」——一旦内部实现重构,测试就碎一地。
verify(userService).find(1L); // 验证协作确实发生
常见坑还有几个:时间相关的逻辑要用 Clock 注入,避免测试里 sleep;随机数要用固定种子;外部 HTTP 调用要打桩而不是真的发请求。这些不稳定因素不去掉,测试就会随机失败,团队很快失去信任。
出现「上周还过、今天挂了」的测试,第一反应不是忽略,而是找到它依赖的不稳定因素并消除。
测试工程实践清单
- 核心业务逻辑先写单元测试,覆盖分支与边界。
- 控制器用 @WebMvcTest + MockMvc,不启动完整应用。
- 数据层敏感 SQL 用 Testcontainers 在真实数据库验证。
- 集成测试用 @Transactional 回滚,保持数据隔离。
- 替身只模拟边界协作,不要过度设计。
- 消除时间、随机、网络等不稳定因素,保证测试可重复。
稳定的测试,是团队敢重构的底气。