CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
软件测试入门/课时/集成测试
RawBlame
课时目录12 节

软件测试入门

  1. 01测试金字塔
  2. 02JUnit 5 基础
  3. 03Mockito 与 Mock
  4. 04Spring Boot 测试
  5. 05MockMvc 接口测试
  6. 06集成测试
  7. 07TDD 与可维护性
  8. 08前端单元测试
  9. 09E2E 与 Playwright
  10. 10覆盖率与质量门禁
  11. 11CI 中的测试
  12. 12综合练习

集成测试

Testcontainers、真实 MySQL/Redis。

35 分钟

集成测试

这一课你将能做到

  1. 说出哪些问题 mock 测不到(SQL、唯一索引、事务、迁移),并归类「可 mock / 必集成 / 不必每次跑」三张清单
  2. 用 Testcontainers + @ServiceConnection 写出对真 MySQL 的插入-查询往返用例,包括唯一索引冲突
  3. 用 *Test / *IT 命名约定与 Failsafe 插件把快慢测分开跑

为什么要学

单元测试把 Mapper mock 掉之后,仍会出现「单测全绿、线上炸库」:SQL 写错、唯一索引没建、事务边界不对、字段映射错位。人话:替身演员台词背得再熟,也不代表真舞台的布景能承重。

集成测试用真实或容器化的 MySQL / Redis,验证多组件一起工作。它贵(秒级起步),所以要克制:只覆盖 mock 无法替代的路径。CodeForge 生产用 MySQL 8 + Redis,集成测试就该连同样的东西,而不是找个内存库自欺欺人。

先建立图像

单元: Service ──mock──► Mapper(假,你编的答案)
集成: Service ──真──► Mapper ──真──► MySQL(Testcontainers 容器)

成本:  启容器 + 建表 + 插入 + 查询  ≫  纯 Mockito
收益:  抓住「SQL / 事务 / 装配 / 迁移」类缺陷
方式优点注意
Testcontainers本地与 CI 一致,版本可钉死需要 Docker
CI services: 起 MySQLworkflow 原生本地要另备库
内存库(H2 等)快方言差异风险大

命名约定:*Test.java 快测(Surefire 默认跑);*IT.java 集成测(Failsafe 在 verify 阶段跑)。

跟着做

步骤 1:先列三张清单,再写代码

打开 backend 的 Mapper / 建表脚本,把场景归类:

  1. 可 mock:纯 Service 分支、DTO 拼装(现有 *ServiceTest 全在此列,保留,不要改成打真库)
  2. 必须集成:userMapper.insert 与唯一索引、事务回滚、复杂查询
  3. 不必每次 PR:Redis + 判题全链路重型场景(可夜间跑)

步骤 2:引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>mysql</artifactId>
    <scope>test</scope>
</dependency>

步骤 3:写第一个真 MySQL 往返用例

Spring Boot 3.1+ 提供 @ServiceConnection,容器连接信息自动注入数据源,比手写 @DynamicPropertySource 更现行:

import com.codeforge.entity.User;
import com.codeforge.mapper.UserMapper;
import com.mybatisflex.core.query.QueryWrapper;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.dao.DuplicateKeyException;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;

import static org.assertj.core.api.Assertions.*;

@SpringBootTest
@Testcontainers
class UserMapperIT {

    @Container
    @ServiceConnection
    static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");

    @Autowired
    UserMapper userMapper;

    private User newUser(String name) {
        User user = new User();
        user.setUsername(name);
        user.setEmail(name + "@example.com");
        user.setPasswordHash("$2a$10$fakehashfakehashfakehash");
        return user;
    }

    @Test
    void 用户写入后可按用户名查回() {
        String name = "t_" + System.nanoTime();   // 每次唯一,防脏数据
        userMapper.insert(newUser(name));

        User loaded = userMapper.selectOneByQuery(
                QueryWrapper.create().where(User::getUsername).eq(name));

        assertThat(loaded).isNotNull();
        assertThat(loaded.getEmail()).isEqualTo(name + "@example.com");
        assertThat(loaded.getPasswordHash()).isNotBlank();
    }

    @Test
    void 用户名唯一索引真实生效() {
        String name = "u_" + System.nanoTime();
        userMapper.insert(newUser(name));

        // 同名再插:mock 永远证不了这一条,只有真库能
        assertThatThrownBy(() -> userMapper.insert(newUser(name)))
            .isInstanceOf(DuplicateKeyException.class);
    }
}

应看到:首次运行拉 mysql:8.0 镜像较慢,之后复用镜像层。表结构由项目启动时的建表初始化(DatabaseInitializer)在容器库里执行,与生产同一套脚本——这正是集成测试的价值:测的是「库结构 + 代码」的组合。

步骤 4:把快慢测分开跑

先用 Maven 的默认约定:*Test 归 Surefire(mvn test),*IT 归 Failsafe。pom.xml 加:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-failsafe-plugin</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>integration-test</goal>
                <goal>verify</goal>
            </goals>
        </execution>
    </executions>
</plugin>
mvn test            # 只跑快测,日常改码用
mvn verify          # 快测 + IT 全跑,提交前用(与 CI 一致)

也可用 JUnit @Tag("integration") 加 Surefire/Failsafe 的 <groups> 过滤,效果等价,团队二选一即可。

步骤 5:集成测试的卫生习惯

  • 数据自备自清:每个用例自己造数(如 System.nanoTime() 后缀),不依赖手工残留行,不改别人的数据
  • 容器复用:容器字段 static + @Container,全类共用一个实例,避免每个用例起停
  • 断言不变量:集成贵,就要买到「数据真的进去又出来」的证据
// 弱:只证明「没炸」
userMapper.insert(user);

// 强:证明往返不变量
User loaded = userMapper.selectOneByQuery(
        QueryWrapper.create().where(User::getUsername).eq(name));
assertThat(loaded.getEmail()).isEqualTo(user.getEmail());

步骤 6:Redis 类需求怎么选层

  • 纯内存限流逻辑(本站 MemoryRequestRateLimiter)→ 单元测即可,已有先例
  • 「缓存写入后读到什么 / TTL 行为」→ 需要真 Redis:加 GenericContainer 或专用 RedisContainer,同样的 @ServiceConnection 写法
  • 不确定时:先单测逻辑,集成只抽一条冒烟

讲清楚要点

  1. mock 答应的是你编的返回值,SQL 正确性只能集成证明:verify(mapper).insert(...) 与「插入真的落库、索引真的拦截」是两回事。
  2. 集成贵,所以用例短、数据自备自清:插入最小行数,断言关键不变量,禁止依赖手工数据。
  3. 镜像钉版本:mysql:8.0 优于浮动 latest——今日绿明日红的 flaky 多半来自环境漂移。
  4. 建表脚本与生产同源:测「库结构 + 代码」组合才有意义;容器里跑项目自己的初始化逻辑。
  5. @ServiceConnection 取代手写动态属性:Boot 3.1+ 自动把容器端口/凭据接进 DataSource,少一份样板、少一个出错点。
  6. 快慢分轨:*Test 挡每个 PR,*IT 在 verify / 主分支 / nightly 档位跑——不是所有集成都要拦每条 push。

你可能会懵的地方

  • MockMvc 都过了,还要 IT 吗? 要。MockMvc + @MockitoBean 可能完全不碰 SQL。IT 回答的是「Mapper、索引、迁移」问题——两层的失败原因根本不同。

  • @Transactional 自动回滚算集成吗? 算(同进程真库 + 回滚隔离)。但注意它会掩盖事务边界问题:被测代码若自己管理事务,外层回滚假设就失效了;异步写入也回滚不到。

  • H2 能代替 MySQL 吗? 简单 CRUD 有时可以,但分页方言、JSON 字段、锁行为差异会让你测了个寂寞。CodeForge 生产是 MySQL,关键路径用容器 MySQL,别拿 H2 的绿灯换安全感。

  • CI 没有 Docker 怎么办? GitHub Actions 的 ubuntu runner 自带 Docker,Testcontainers 开箱即用;自建环境可把 IT 挪到 nightly job,用 services: 起库替代。

  • 为什么我的 IT 偶发「connection refused」? 容器还没就绪测试就开始了。@ServiceConnection + Spring 会等容器,但自写的连接逻辑要确认顺序;本地踩到多半是复用了已停止的旧容器实例。

错法对照

错法问题正解
所有测试都打真库套件无人愿跑底多中少,IT 只管 SQL/事务
IT 依赖开发者手工数据不可重复用例自插入 + 唯一后缀
镜像用 latest环境漂移型 flaky钉死 mysql:8.0
每个用例起一个新容器分钟级浪费static 容器全类复用
用 IT 测纯 if-else大炮打蚊子退回 Mockito

卡点急救

  1. 本机没 Docker,IT 全红:装 Docker Desktop / WSL2 后端;或临时 @Disabled("需 Docker") 并把验证交给 CI 的 verify 阶段。
  2. 首次拉镜像超时:配置国内镜像加速,或先 docker pull mysql:8.0 预热;Testcontainers 复用镜像层后二次运行很快。
  3. IT 里 Started CodeForgeApplication 后报连接失败:确认用的是 @ServiceConnection 而不是写死的 localhost:3306——容器端口是随机的,写死必挂。

面试常问

  • 为什么用 Testcontainers 而不是 H2 跑集成测试? 测试的价值取决于环境与生产的相似度。H2 与 MySQL 在方言、索引行为、隔离级别上有差异,会出现「H2 绿、MySQL 炸」。Testcontainers 起真实 MySQL 8 容器,本地与 CI 环境一致,镜像可钉版本,代价是需要 Docker 与秒级启动——对关键路径值得。

  • 举一个「单测绿、集成红」的缺陷。 用户名唯一索引:单测 mock 了 insert 返回 1,一切正常;真库插入重复名抛 DuplicateKeyException,若 Service 没接住就是 500。还有 Flyway 迁移漏执行、事务内异步发消息被外层回滚等,都是 mock 测不到的类型。

  • 集成测试 flaky 怎么治理? 三招:环境钉版本(镜像固定 tag)、数据隔离(用例自备自清 + 唯一后缀)、运行分轨(快测挡 PR,IT 放 verify/nightly)。稳定性一半来自断言质量,一半来自环境卫生。

自测三问

  1. 举一个「单测绿、集成红」的真实缺陷类型。
  2. @ServiceConnection 解决了 Testcontainers 随机端口的什么问题?
  3. 为什么建议 *Test 与 *IT 分开跑?各挂在 Maven 哪个阶段?

小结一句话

集成测试贵而准:只让 SQL、事务、唯一索引这些 mock 证不了的事上真库,其余仍交给 Mockito。

延伸

  • Testcontainers 官方文档
  • Spring Boot · Testcontainers 支持
  • 下一课:test-tdd 把前面的手法组织成「红 → 绿 → 重构」的开发节奏,让测试驱动设计。
加载本节练习…
← 上一课MockMvc 接口测试下一课 →TDD 与可维护性