这是我学习集成测试时整理的笔记。单元测试保证"每个零件是好的",但零件拼起来不一定转得动——接口对不上、数据库字段不匹配、事务边界不对,这些单元测试测不出来。集成测试就是解决"拼起来对不对"这个问题的。
一、先理解测试金字塔
测试不是越多越好,而是按金字塔结构分配:
| 层级 | 测什么 | 特点 |
|---|---|---|
| 单元测试 | 单个类/方法 | 快(毫秒级)、Mock 外部依赖、问题定位到行 |
| 集成测试 | 模块协作 | 中(秒级)、起真环境(DB/MQ/HTTP)、验证接口契约 |
| E2E 测试 | 完整业务流程 | 慢(分钟级)、全链路、覆盖最少 |
核心思想:单元测试 70% 打底,集成测试 20% 验证协作,E2E 10% 兜底。集成测试是单元测试的补充,不是替代——它专门抓"单测测不到、上线才爆"的那类问题。
二、集成测试到底测什么
集成测试的目标是验证真实协作,典型的被测对象:
| 被测对象 | 集成测试验证点 |
|---|---|
| DAO + 数据库 | SQL 对不对、字段映射对不对、事务回滚对不对 |
| Service + MQ | 消息发没发、消费逻辑对不对、重试行为 |
| Controller + HTTP | 请求参数绑定、响应格式、状态码、异常处理 |
| 多个 Service 协作 | 跨服务调用链、分布式事务边界 |
判断标准:涉及两个以上模块"真实交互"的测试,就是集成测试(哪怕只用到真实数据库、其余 Mock)。
三、Spring Boot 集成测试实战
以"DAO + 数据库"为例,Spring Boot 集成测试标准写法:
1. 依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- 用 Testcontainers 起真实数据库(推荐,比 H2 更接近生产) -->
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>2. 测试类
java
@SpringBootTest
@Testcontainers
class UserMapperTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0")
.withDatabaseName("test")
.withUsername("test")
.withPassword("test");
@DynamicPropertySource
static void props(DynamicPropertyRegistry registry) {
// 把测试容器的地址注入到 Spring 配置
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Autowired
private UserMapper userMapper;
@Test
void 查询用户_能拿到正确的字段() {
User user = userMapper.selectById(1L);
assertNotNull(user);
assertEquals("BinMaker", user.getName());
}
}关键点:
@SpringBootTest:起完整 Spring 容器(真实的 Bean 注入)Testcontainers:用 Docker 起真实 MySQL,不是 H2 模拟——SQL 方言、事务行为和生产一致@DynamicPropertySource:把容器的连接信息动态注入配置
3. 测试数据隔离(重要)
集成测试操作真实数据库,测试之间必须隔离,否则互相污染:
java
@SpringBootTest
@Transactional // 每个测试跑完自动回滚,数据不落地
class OrderServiceTest { ... }@Transactional:测试方法包裹事务,执行完回滚——最简单有效的隔离方式- 需要真实提交的场景(如异步消息)用
@Commit或单独清理数据
4. Controller 层集成测试(MockMvc)
java
@SpringBootTest
@AutoConfigureMockMvc
class OrderControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void 创建订单_返回201() throws Exception {
mockMvc.perform(post("/api/orders")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"goodsId\": 1, \"num\": 2}"))
.andExpect(status().isCreated())
.andExpect(jsonPath("$.orderNo").exists());
}
}MockMvc 走完整的 Spring MVC 链路(参数绑定、过滤器、异常处理器),但不发真实 HTTP 请求——比启动真实服务快,又比纯单元测试真实。
四、集成测试 vs 单元测试(一表看懂)
| 维度 | 单元测试 | 集成测试 |
|---|---|---|
| 被测范围 | 单个类/方法 | 多个模块协作 |
| 外部依赖 | Mock | 真实(DB/MQ)或轻量替代 |
| 速度 | 毫秒级 | 秒级 |
| 问题定位 | 精确到代码行 | 定位到"哪个协作环节" |
| 跑得频率 | 每次提交都跑 | CI 里跑(或提交时跑关键路径) |
| 典型框架 | JUnit + Mockito | @SpringBootTest + Testcontainers |
五、常见坑(都踩过)
- 测试数据互相污染:测试 A 插入的数据影响测试 B → 用
@Transactional回滚或每测前清表 - H2 与生产不一致:H2 模拟 MySQL 可能"本地过、上线挂"(SQL 方言差异)→ 用 Testcontainers 起真实库
- 容器启动慢:Testcontainers 每次拉镜像/起容器耗时 → 用
static @Container复用(整个测试类一个容器) - 配置泄漏:集成测试误连生产库 → 测试环境连接串必须显式注入,禁止读生产配置
- 和单元测试混跑:集成测试慢,别和单元测试混在一个任务里每次都跑全量 → CI 拆分(单测快任务 + 集成慢任务)
小结
- 集成测试 = 验证"模块拼起来对不对",是单元测试的互补不是替代
- 测试金字塔:单元 70% + 集成 20% + E2E 10%
- Spring Boot 标准姿势:
@SpringBootTest+Testcontainers(真实 DB)+@Transactional(回滚隔离)+MockMvc(Controller) - 测试隔离用回滚、真实环境用容器、别混跑慢测试
下一篇可以看我的《JUnit 5 快速入门》(断言怎么写、生命周期、Mockito),或者《JMeter 快速入门》了解压测层面的验证。
