JUnit5+Spring+Docker集成测试:@Sql执行时机问题求助
我来帮你搞定这个问题!你遇到的核心痛点就是Spring的@Sql注解跑脚本的时机太早,PostgreSQL Docker容器还没完全就绪呢,脚本就已经执行了,导致测试失败对吧?下面给你几个靠谱的解决方案,从最省心的官方集成到灵活自定义的方式都有:
方案1:改用Testcontainers官方集成(首推)
放弃你现在用的第三方@Docker注解,换成Testcontainers的官方JUnit5支持,它和Spring的集成非常成熟,能自动帮你协调容器启动、就绪等待和Spring上下文初始化的顺序,根本不用操心脚本执行时机的问题。
具体步骤:
- 先添加Testcontainers的相关依赖(如果还没加的话):
(比如Maven项目添加org.testcontainers:junit-jupiter和org.testcontainers:postgresql依赖) - 修改你的测试类,用Testcontainers的注解替代原来的
@Docker:
@Testcontainers @ExtendWith(SpringExtension::class) @Sql(scripts = ["/data/clear.sql", "/data/operations.sql"]) @ContextConfiguration(classes = [DockerContainerConfig::class]) class OperationSearchRepositoryImplTest { companion object { // 定义PostgreSQL容器,配置参数和等待条件 @Container val postgresContainer = PostgreSQLContainer("postgres:9.6") .withDatabaseName("nv2test") .withUsername("postgres") .withPassword("postgres") .waitingFor(Wait.forLogMessage(".*database system is ready to accept connections.*", 1)) // 动态向Spring注册数据源配置,让Spring自动使用容器的连接信息 @JvmStatic @DynamicPropertySource fun registerDataSourceProperties(registry: DynamicPropertyRegistry) { registry.add("spring.datasource.url", postgresContainer::getJdbcUrl) registry.add("spring.datasource.username", postgresContainer::getUsername) registry.add("spring.datasource.password", postgresContainer::getPassword) } } @Test fun shouldFindByExample() { // 你的测试逻辑 } }
- 调整你的
DockerContainerConfig:要么让它读取动态注册的数据源属性,要么直接用Spring Boot的自动配置(如果是Spring Boot项目的话),这样数据源会自动关联到就绪的容器。
这个方案的好处是完全不用手动处理顺序问题,Testcontainers会确保容器完全就绪后,才让Spring初始化数据源,之后@Sql脚本自然就会在正确的时机执行。
方案2:自定义TestExecutionListener控制脚本执行时机
如果你不想换用Testcontainers,想继续用现有的@Docker注解,那可以自定义一个TestExecutionListener,先等容器就绪,再触发@Sql脚本的执行。
具体步骤:
- 写一个自定义的Listener:
class DockerReadySqlListener : AbstractTestExecutionListener() { override fun getOrder(): Int { // 让这个Listener比SqlScriptsTestExecutionListener先执行 return SqlScriptsTestExecutionListener.DEFAULT_ORDER - 1 } override fun beforeTestClass(testContext: TestContext) { // 获取你用的Docker扩展实例,等待容器就绪 // 这里的DockerExtension要替换成你实际用的那个扩展类 val dockerExtension = testContext.testExecutionListeners .filterIsInstance<DockerExtension>() .firstOrNull() dockerExtension?.let { // 调用扩展的等待方法,确保容器完全就绪 it.waitForContainers(testContext) } } }
- 在测试类中注册这个自定义Listener:
@Docker( image = "postgres:9.6", ports = [Port(exposed = 5433, inner = 5432)], environments = [ Environment(key = "POSTGRES_DB", value = "nv2test"), Environment(key = "POSTGRES_USER", value = "postgres"), Environment(key = "POSTGRES_PASSWORD", value = "postgres")], waitFor = WaitFor("LOG: database system is ready to accept connections"), newForEachCase = false ) @Sql(scripts = ["/data/clear.sql", "/data/operations.sql"]) @ExtendWith(SpringExtension::class) @TestExecutionListeners( value = [ DockerReadySqlListener::class, SqlScriptsTestExecutionListener::class, DependencyInjectionTestExecutionListener::class ], mergeMode = TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS ) @ContextConfiguration(classes = [DockerContainerConfig::class]) class OperationSearchRepositoryImplTest{ @Test fun shouldFindByExample() { // 你的测试逻辑 } }
这个方法通过调整Listener的执行顺序,强制先等待容器就绪,再执行SQL脚本,完美解决顺序问题。
方案3:手动执行SQL脚本(最灵活)
如果上面两种方案都不适合你,那可以干脆去掉@Sql注解,手动在测试类中控制脚本的执行时机,确保容器和数据源都就绪后再跑脚本。
具体步骤:
@Docker(...) // 保留你原来的@Docker注解 @ExtendWith(SpringExtension::class) @ContextConfiguration(classes = [DockerContainerConfig::class]) class OperationSearchRepositoryImplTest { @Autowired lateinit var jdbcTemplate: JdbcTemplate @BeforeAll fun setupDatabase() { // 执行SQL脚本,这里用Spring的ScriptUtils来处理脚本文件 executeSqlScript("/data/clear.sql") executeSqlScript("/data/operations.sql") } private fun executeSqlScript(scriptPath: String) { val scriptResource = ClassPathResource(scriptPath) ScriptUtils.executeSqlScript(jdbcTemplate.dataSource!!.connection, scriptResource) } @Test fun shouldFindByExample() { // 你的测试逻辑 } }
这个方式完全由你掌控脚本的执行时机,@BeforeAll方法会在所有测试方法之前执行,这时候容器和数据源肯定已经就绪了。
内容的提问来源于stack exchange,提问作者Sam Clarenc
相关产品推荐
相关产品推荐

