《Spring Boot + JPA 实战》第18招 别只用H2测试
01、痛点
我是老李。最近复盘一个线上事故:团队用 Spring Boot + JPA 开发订单服务,本地和 CI 里所有 Repository 测试、Service 测试都跑在 H2 内存库上,结果全绿。发布到 MySQL 8.0 后,建表阶段就出问题,有个时间字段的默认值没生效,接口写入时直接报字段长度超限;还有一个查询在测试环境毫秒级,生产却锁表,拖垮整条链路。大家第一反应是“测试怎么没抓住”。其实不是测试没抓住,而是测试环境把最关键的差异藏起来了。H2 和 MySQL 是两套数据库,方言、DDL、锁、事务隔离级别都不一样。只拿 H2 当数据层测试,等于在模拟器里练开车,上路才发现离合器、刹车、盲区完全不是一回事。现在微服务和容器化普及,CI 里跑真实数据库已经不是奢侈品,而是必须补上的一课。所以今天把这套升级方法讲透。
02、是什么
很多人把数据层测试理解成“测试 Repository 的 Java 代码”。这很片面。真正的数据层由四部分组成:ORM 映射、JDBC 驱动、数据库方言和真实 Schema 与存储引擎。H2 测试只覆盖了 ORM 映射和 Java 方法,却把驱动、方言、Schema 换成了另一个东西。MySQL 上才会暴露的问题就包括:字段类型映射、索引建立、自增值、事务隔离级别、行锁与间隙锁、原生 SQL 函数。@DataJpaTest 是一个切片测试注解,默认为了快,会把数据源替换成嵌入式数据库,也容易让人误以为数据层已经验证通过。Integration Test 是更高一层的集成测试,启动更多上下文;而 Testcontainers 是一个 Java 测试库,通过 Docker 启动一次性真实数据库容器,可以指定 MySQL 版本、配置账号和初始化脚本。Docker Compose 则用于本地开发环境,拉起整个依赖组合。把它们组合起来,才能让 CI 的数据层能力接近生产。
03、为什么
H2 测试全通过,MySQL 上线仍失败,根本原因有三个。第一,SQL 方言差异。JPA 的 ddl-auto 会根据方言生成不同 DDL。比如 H2 里能创建的类型,在 MySQL 下可能变成 TEXT 或 TINYINT,索引长度、字符集、排序规则都可能不同。第二,DDL 差异。H2 默认宽松,MySQL 严格模式会拦截很多问题,例如非空默认值、无符号、自增锁、唯一键冲突。第三,事务和锁差异。H2 的 MVCC 实现和 InnoDB 差别很大,同样的隔离级别,在 H2 下不会死锁,在 MySQL 下可能因间隙锁导致阻塞。另外,原生 SQL 是最容易暴露方言差异的地方:limit 语法、日期格式、字符串拼接、强制索引等。如果测试只跑 H2,这些差异全部被掩盖。数据层测试真正需要验证的,不是 Repository 的 Java 逻辑,而是 ORM + Driver + Schema + Database 这个组合是否一致。因此必须用与生产同类型的数据库做集成测试。
04、架构
有效的数据层测试应该分层。最底层是纯单元测试,只测 Service 或领域逻辑,Repository 用 mock。中间层是切片测试,用 @DataJpaTest 加 H2,在开发期快速反馈 JPA 映射和 JPQL 是否正确。最高层是真实数据库集成测试,用 Testcontainers MySQL,覆盖 Repository、原生 SQL、事务和并发锁。这个顶层不应被省掉。CI 架构上,建议将测试任务拆成 fast 和 slow 两组:fast 跑普通单元测试和 H2 切片测试,slow 跑 Testcontainers 集成测试。Docker 环境在 CI 中作为基础依赖,测试启动时拉取 mysql:8.0 镜像,容器生命周期由测试框架管理。为了避免每次测试都重新启动容器降低效率,可以在固定端口上复用容器,或者使用 Spring Boot 3.1 的 @ServiceConnection 自动注入;本地开发则用 Docker Compose 同时启动 MySQL、Redis、消息队列等。这样从开发到 CI 再到生产,数据库版本和配置保持一致。
05、实战
第一步,依赖准备。在测试依赖中引入 Spring Boot Test、Testcontainers、JUnit 5 以及 MySQL 驱动。MySQL 镜像固定为 mysql:8.0.36,避免 CI 缓存漂移。第二步,编写测试基类。在测试基类中添加 @Testcontainers,声明一个静态的 MySQLContainer 属性,并通过 @Container 和 @ServiceConnection 注解管理。镜像参数设置库名、账号、密码,并显式指定字符集为 utf8mb4。这样 Spring Boot 会自动把数据源指向容器。第三步,改造 Repository 测试。去掉 @DataJpaTest 默认的嵌入式替换,使用 @AutoConfigureTestDatabase(replace = NONE) 并导入 Testcontainers 配置。测试中真实写入数据,断言事务、级联、唯一约束都按 MySQL 行为生效。第四步,验证原生 SQL。对于性能敏感或方言相关的查询,使用原生 SQL 或 JdbcTemplate 执行 SELECT ... FOR UPDATE、JSON 函数、ON DUPLICATE KEY UPDATE 等语句,确保 MySQL 真正支持。第五步,测试并发锁。用两个独立线程和两个事务模拟并发场景:一个事务开启 SELECT ... FOR UPDATE 持锁,另一个事务尝试更新同一行,设置锁等待超时,断言最终结果和异常符合 InnoDB 的锁行为。这一步能提前发现死锁和间隙锁问题。第六步,CI 集成。在 CI 流水线中给任务配置 Docker 环境,先拉取镜像,再执行 Maven 或 Gradle 的集成测试阶段。为了提速,可把 mysql:8.0 镜像作为缓存,使用固定 testcontainers.reuse 配置复用容器。整个流程下来,核心 Repository 的反馈从“假绿”变成“真绿”。
06、洞见
经历几次事故后我意识到,数据层测试不是测试 Repository 这个类,而是测试一整个组合:ORM 生成 SQL 的方式、JDBC 驱动对数据库协议的实现、Schema 迁移脚本、数据库版本和配置。这四者任何一处不一致,都可能导致线上故障。H2 测试快,但它验证的是另一套组合。MySQL Testcontainers 慢一点,但验证的是和生产接近的组合。因此,正确的策略不是用 MySQL 替换所有 H2,而是让 H2 负责快速反馈,让 MySQL Testcontainers 负责准生产验证。尤其对核心交易链路、库存扣减、订单状态流转这些涉及锁和事务的场景,必须跑真实数据库。测试数据库的版本要尽量和生产一致,不能生产是 MySQL 8.0,测试用 MySQL 5.7。另外,真实数据库能暴露执行计划问题,比如索引缺失导致全表扫描,H2 完全看不到。总之,测试环境离生产越近,上线越稳。
07、行动
建议分四步推进。第一步,盘点现有 Repository 测试,选出涉及原生 SQL、复杂事务、锁竞争、唯一约束的核心模块,优先升级。第二步,在项目里统一加入 Testcontainers 基类和配置,让团队其他成员可以直接继承,降低迁移成本。第三步,保留 H2 用于开发期冒烟,但在 CI 中增加一个真实数据库集成测试任务,作为合并门禁。第四步,逐步提高测试覆盖率,将订单、支付、库存等关键数据链路全部纳入 MySQL Testcontainers。不要等到线上出事故才回头补课。CI 环境需要安装 Docker,并给 Docker 镜像仓库配置访问权限。如果内网无法直接拉镜像,可以提前把镜像导入私有仓库或使用 Docker Compose 启动外部数据库,但要确保 CI 里数据库版本一致。最后,持续监控集成测试耗时,必要时并行执行或按模块拆分,避免拖慢反馈。
08、小结
H2 测试全通过,只能说明代码在 H2 上没问题,不能证明在 MySQL 上没问题。数据层是 ORM、驱动、Schema、数据库四者的组合,必须用真实数据库测试这个组合。Testcontainers 让真实数据库测试变得简单可复制,把 CI 环境拉近生产。核心动作就是:把关键 Repository 从纯 H2 升级为 MySQL Testcontainers。别让测试假绿成为上线的第一颗雷。
09、链接与概念说明
- @DataJpaTest:Spring Boot 的 JPA 切片测试注解,只初始化与 JPA 相关的上下文,默认会替换成嵌入式数据库。
- Integration Test:集成测试,验证多个组件或外部依赖之间的协作,通常启动完整或部分应用上下文。
- Testcontainers:JVM 测试库,通过 Docker 启动临时容器,支持 MySQL、PostgreSQL、Kafka 等,常用于数据库集成测试。
- Docker Compose:本地容器编排工具,用 YAML 文件定义 MySQL、Redis 等依赖,一条命令启动整套环境。
- InnoDB 锁:MySQL 默认存储引擎,采用行锁、间隙锁、临键锁和 MVCC,事务行为与 H2 差异大。
- 原生 SQL:直接使用数据库方言写的 SQL,绕过 JPQL,能使用数据库特有的函数和语法,但也会放大方言差异。
- 建议延伸阅读:Spring Boot Testcontainers 官方支持文档;Testcontainers 官方文档;MySQL 8.0 Reference Manual 中关于事务隔离级别和锁的章节。

浙公网安备 33010602011771号