Skip to content

这是我学习集成测试时整理的笔记。单元测试保证"每个零件是好的",但零件拼起来不一定转得动——接口对不上、数据库字段不匹配、事务边界不对,这些单元测试测不出来。集成测试就是解决"拼起来对不对"这个问题的。

一、先理解测试金字塔

测试不是越多越好,而是按金字塔结构分配:

测试金字塔

层级测什么特点
单元测试单个类/方法快(毫秒级)、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

五、常见坑(都踩过)

  1. 测试数据互相污染:测试 A 插入的数据影响测试 B → 用 @Transactional 回滚或每测前清表
  2. H2 与生产不一致:H2 模拟 MySQL 可能"本地过、上线挂"(SQL 方言差异)→ 用 Testcontainers 起真实库
  3. 容器启动慢:Testcontainers 每次拉镜像/起容器耗时 → 用 static @Container 复用(整个测试类一个容器)
  4. 配置泄漏:集成测试误连生产库 → 测试环境连接串必须显式注入,禁止读生产配置
  5. 和单元测试混跑:集成测试慢,别和单元测试混在一个任务里每次都跑全量 → CI 拆分(单测快任务 + 集成慢任务)

小结

  • 集成测试 = 验证"模块拼起来对不对",是单元测试的互补不是替代
  • 测试金字塔:单元 70% + 集成 20% + E2E 10%
  • Spring Boot 标准姿势:@SpringBootTest + Testcontainers(真实 DB)+ @Transactional(回滚隔离)+ MockMvc(Controller)
  • 测试隔离用回滚、真实环境用容器、别混跑慢测试

下一篇可以看我的《JUnit 5 快速入门》(断言怎么写、生命周期、Mockito),或者《JMeter 快速入门》了解压测层面的验证。