
本文详解如何解决 Spring Boot 集成测试中 Liquibase 在 Testcontainers 启动时重复执行预置数据变更集(如 insert_test_user.yaml),引发 duplicate key violates unique constraint 异常的问题。核心在于正确配置 Testcontainers 的 JDBC URL 与驱动,确保 Liquibase 每次都操作全新、隔离的临时数据库实例。
本文详解如何解决 spring boot 集成测试中 liquibase 在 testcontainers 启动时重复执行预置数据变更集(如 insert_test_user.yaml),引发 `duplicate key violates unique constraint` 异常的问题。核心在于正确配置 testcontainers 的 jdbc url 与驱动,确保 liquibase 每次都操作**全新、隔离的临时数据库实例**。
在基于 Spring Boot + Testcontainers + Liquibase 的集成测试中,一个典型却易被忽视的问题是:Liquibase 在每次测试上下文启动时,都会重新执行所有变更集(包括用于测试准备的 INSERT 脚本)。当多个测试类共享同一数据库连接配置(尤其是硬编码指向固定 PostgreSQL 实例,如 jdbc:postgresql://localhost:5432/postgres),或未启用 Testcontainers 的自动容器生命周期绑定时,Liquibase 就可能反复向同一个物理数据库表中插入相同主键/唯一字段的数据(例如重复邮箱 <a class="__cf_email__" data-cfemail="660c090e084802090326031e070b160a034805090b" href="/cdn-cgi/l/email-protection">[email protected]</a>),最终触发 ERROR: duplicate key value violates unique constraint "user_email_key"。
根本原因在于:你当前的 application-test.properties 中配置的是静态本地数据库连接:
spring.datasource.url=jdbc:postgresql://localhost:5432/postgres spring.datasource.driver-class-name=org.postgresql.Driver
该配置使 Liquibase 绕过了 Testcontainers 管理的临时容器,直接连接到宿主机上长期存在的 PostgreSQL 实例(甚至可能是其他测试残留的库)。而 @Container 声明的 PostgreSQLContainer 虽已启动,却未被 Spring 数据源和 Liquibase 实际使用——导致“容器启了,但没人用”。
✅ 正确解法是:强制 Spring Boot 使用 Testcontainers 提供的透明代理驱动与动态 JDBC URL,让整个数据访问栈(包括 Liquibase 初始化)均指向由 Testcontainers 动态创建、独占、且随测试生命周期自动销毁的数据库实例。
在 src/test/resources/application-test.properties 中,替换为以下两行:
spring.datasource.driver-class-name=org.testcontainers.jdbc.ContainerDatabaseDriver spring.datasource.url=jdbc:tc:postgresql://localhost:5432/test
? 关键说明:
org.testcontainers.jdbc.ContainerDatabaseDriver是 Testcontainers 提供的代理驱动,能解析jdbc:tc:协议并自动启动/复用对应容器;jdbc:tc:postgresql://localhost:5432/test中的test是数据库名(可自定义),Testcontainers 会为每个测试 JVM 创建全新的、空的test库,彻底隔离各测试轮次;- 此配置下,Liquibase 启动时操作的是一个干净的、无任何历史数据的数据库,
insert_test_user.yaml只会成功执行一次(且不会与其他测试冲突)。
? 额外建议与验证步骤:
- ✅ 确保
testcontainers和postgresql依赖已声明(Gradle 示例):testImplementation 'org.testcontainers:testcontainers:1.19.7' testImplementation 'org.testcontainers:postgresql:1.19.7'
- ✅ 删除
BaseIntegrationTest中手动初始化PostgreSQLContainer的代码(如@Container PostgreSQLContainer postgreSQLContainer = ...),因为jdbc:tc:协议已隐式管理容器生命周期;若需显式控制(如暴露端口),可保留但必须配合@ServiceConnection(Spring Boot 3.1+)或@DynamicPropertySource注入 URL,避免双重配置。 - ✅ 运行单个测试后,检查控制台日志是否包含类似
Started PostgreSQLContainer...和Creating database schema—— 这表明 Liquibase 正确作用于 Testcontainers 实例。 - ⚠️ 切勿在测试中混合使用
userRepository.save(...)手动插入与 Liquibase 自动插入相同业务数据(如重复邮箱),否则即使容器隔离,单次测试内仍可能因事务/缓存引发冲突。
通过这一配置切换,你将获得真正可靠的、可重复的集成测试环境:每个测试运行在专属数据库快照上,Liquibase 变更集安全执行,控制器逻辑(如用户创建的唯一性校验)得以精准验证。

















