From 2328e7ebe27fad76fb0d04bbdd2dece4986a2e5e Mon Sep 17 00:00:00 2001 From: Swordsteel Date: Tue, 12 Aug 2025 13:01:15 +0200 Subject: [PATCH] Rewrite postgres test container --- README.md | 17 ++--- .../test/container/PostgresContainer.kt | 12 ---- .../hlaeja/test/container/PostgresExecutor.kt | 55 --------------- .../test/container/PostgresInitializer.kt | 67 ------------------ .../test/container/PostgresTestContainer.kt | 15 ++++ .../extension/PostgresTestExtension.kt | 21 ++++++ .../postgres/PostgresTestListener.kt | 24 +++++++ .../postgres/TestContainerPostgres.kt | 68 +++++++++++++++++++ .../test/container/util/ContainerUtil.kt | 12 ++++ .../ltd/hlaeja/test/util/ContainerUtil.kt | 26 ------- 10 files changed, 147 insertions(+), 170 deletions(-) delete mode 100644 src/main/kotlin/ltd/hlaeja/test/container/PostgresContainer.kt delete mode 100644 src/main/kotlin/ltd/hlaeja/test/container/PostgresExecutor.kt delete mode 100644 src/main/kotlin/ltd/hlaeja/test/container/PostgresInitializer.kt create mode 100644 src/main/kotlin/ltd/hlaeja/test/container/PostgresTestContainer.kt create mode 100644 src/main/kotlin/ltd/hlaeja/test/container/extension/PostgresTestExtension.kt create mode 100644 src/main/kotlin/ltd/hlaeja/test/container/postgres/PostgresTestListener.kt create mode 100644 src/main/kotlin/ltd/hlaeja/test/container/postgres/TestContainerPostgres.kt create mode 100644 src/main/kotlin/ltd/hlaeja/test/container/util/ContainerUtil.kt delete mode 100644 src/main/kotlin/ltd/hlaeja/test/util/ContainerUtil.kt diff --git a/README.md b/README.md index cfa3386..d50123d 100644 --- a/README.md +++ b/README.md @@ -2,22 +2,19 @@ In the forge of software development, where annotations ignite, A crucible of testing, common classes to excite. Each annotation examined, with attention to detail and might, Their effects on code behavior, tested through day and night. From mockk objects to test doubles, a toolkit to refine, Developers and testers, their skills to redefine. The Annotation Validator, a sentinel of code integrity true, A library of verification, where testing wisdom shines anew. - ## Postgres Test Container -`@PostgresContainer` Annotation for integration tests. +`@PostgresTestContainer` Annotation for integration tests. -Initialize Postgres test container using spring properties for R2DBC, -script located in `src//resources` folder. And specify properties in `src//resources/application.properties`. +Initialize Postgres test container using test container default properties, script located in `src//resources/postgres/` folder. ### Properties For Test Container -| name | required | example | info | -|----------------------------|:--------:|-------------|----------------------------------------------------------------| -| container.postgres.version | | postgres:17 | postgres container default postgres:latest | -| container.postgres.init | ✓ | schema.sql | Postgres init script containing all structure and functions | -| container.postgres.before | | data.sql | Postgres data script containing all data to populate database | -| container.postgres.after | | reset.sql | Postgres reset script containing all command to reset database | +| file | required | info | +|---------------------|:--------:|----------------------------------------------------------------| +| postgres/schema.sql | ✓ | Postgres init script containing all structure and functions | +| postgres/data.sql | | Postgres data script containing all data to populate database | +| postgres/reset.sql | | Postgres reset script containing all command to reset database | ## Releasing library diff --git a/src/main/kotlin/ltd/hlaeja/test/container/PostgresContainer.kt b/src/main/kotlin/ltd/hlaeja/test/container/PostgresContainer.kt deleted file mode 100644 index eb072dc..0000000 --- a/src/main/kotlin/ltd/hlaeja/test/container/PostgresContainer.kt +++ /dev/null @@ -1,12 +0,0 @@ -package ltd.hlaeja.test.container - -import org.springframework.test.context.ContextConfiguration -import org.springframework.test.context.TestExecutionListeners -import org.springframework.test.context.TestExecutionListeners.MergeMode.MERGE_WITH_DEFAULTS - -@Suppress("unused") -@Retention(AnnotationRetention.RUNTIME) -@Target(AnnotationTarget.CLASS) -@ContextConfiguration(initializers = [PostgresInitializer::class]) -@TestExecutionListeners(listeners = [PostgresInitializer::class], mergeMode = MERGE_WITH_DEFAULTS) -annotation class PostgresContainer diff --git a/src/main/kotlin/ltd/hlaeja/test/container/PostgresExecutor.kt b/src/main/kotlin/ltd/hlaeja/test/container/PostgresExecutor.kt deleted file mode 100644 index 0d0142a..0000000 --- a/src/main/kotlin/ltd/hlaeja/test/container/PostgresExecutor.kt +++ /dev/null @@ -1,55 +0,0 @@ -package ltd.hlaeja.test.container - -import io.github.oshai.kotlinlogging.KotlinLogging -import java.io.BufferedReader -import java.io.InputStream -import java.io.InputStreamReader -import kotlinx.coroutines.runBlocking -import ltd.hlaeja.test.util.isResourceFile -import org.springframework.r2dbc.core.DatabaseClient -import org.springframework.r2dbc.core.await -import org.springframework.stereotype.Component - -private val log = KotlinLogging.logger {} - -@Component -class PostgresExecutor( - private val databaseClient: DatabaseClient, -) { - - fun executeSqlFile( - sqlFile: String, - ) = runBlocking { - sqlFile.isResourceFile() - ?.inputStream - ?.use { - log.debug { "Executing SQL file: $sqlFile" } - executeSqlStatements(makeSqlStatements(it)) - } - ?: log.debug { "SQL file not found or not readable: $sqlFile" } - } - - @Suppress("TooGenericExceptionThrown") - private suspend fun executeSqlStatements( - statements: List, - ) = try { - statements.forEach { statement -> - log.debug { "Running statement: $statement" } - databaseClient.sql(statement).await() - } - } catch (e: Exception) { - throw RuntimeException("Failed to execute SQL statements", e) - } - - private suspend fun makeSqlStatements( - inputStream: InputStream, - ): List = BufferedReader(InputStreamReader(inputStream)) - .lines() - .filter { it.isNotEmpty() && !it.startsWith("--") } - .map { it.trim() } - .toList() - .joinToString(" ") - .split(';') - .filter { it.isNotBlank() } - .map { "${it.trim()};" } -} diff --git a/src/main/kotlin/ltd/hlaeja/test/container/PostgresInitializer.kt b/src/main/kotlin/ltd/hlaeja/test/container/PostgresInitializer.kt deleted file mode 100644 index 8e163b3..0000000 --- a/src/main/kotlin/ltd/hlaeja/test/container/PostgresInitializer.kt +++ /dev/null @@ -1,67 +0,0 @@ -package ltd.hlaeja.test.container - -import io.github.oshai.kotlinlogging.KotlinLogging -import ltd.hlaeja.test.util.getProperty -import ltd.hlaeja.test.util.isResourceFile -import org.springframework.boot.test.util.TestPropertyValues -import org.springframework.context.ApplicationContextInitializer -import org.springframework.context.ConfigurableApplicationContext -import org.springframework.test.context.TestContext -import org.springframework.test.context.TestExecutionListener -import org.testcontainers.containers.PostgreSQLContainer - -private val log = KotlinLogging.logger {} - -@Suppress("unused") -class PostgresInitializer : ApplicationContextInitializer, TestExecutionListener { - - companion object { - const val SCRIPT_INIT = "container.postgres.init" - const val SCRIPT_BEFORE = "container.postgres.before" - const val SCRIPT_AFTER = "container.postgres.after" - const val POSTGRES_VERSION = "container.postgres.version" - const val POSTGRES_LATEST = "postgres:latest" - } - - override fun initialize( - context: ConfigurableApplicationContext, - ) { - postgres(context).apply { - TestPropertyValues.of( - "spring.r2dbc.url=r2dbc:pool:postgresql://$host:$firstMappedPort/$databaseName", - "spring.r2dbc.username=$username", - "spring.r2dbc.password=$password", - ).applyTo(context) - } - } - - override fun beforeTestClass( - context: TestContext, - ) { - context.testClass - .also { log.debug { "Starting execution before class: ${it.simpleName}" } } - .getAnnotation(PostgresContainer::class.java) ?: return - context.getProperty(SCRIPT_BEFORE) - ?.let { context.applicationContext.getBean(PostgresExecutor::class.java).executeSqlFile(it) } - } - - override fun afterTestClass( - context: TestContext, - ) { - context.testClass - .also { log.debug { "Starting execution after class: ${it.simpleName}" } } - .getAnnotation(PostgresContainer::class.java) ?: return - context.getProperty(SCRIPT_AFTER) - ?.let { context.applicationContext.getBean(PostgresExecutor::class.java).executeSqlFile(it) } - } - - private fun postgres( - context: ConfigurableApplicationContext, - ): PostgreSQLContainer<*> = PostgreSQLContainer(context.getProperty(POSTGRES_VERSION, POSTGRES_LATEST)).apply { - context.getProperty(SCRIPT_INIT) - ?.isResourceFile() - ?.let { lala -> withInitScript(lala.path) } - ?: log.error { "Postgres init script not found" } - start() - } -} diff --git a/src/main/kotlin/ltd/hlaeja/test/container/PostgresTestContainer.kt b/src/main/kotlin/ltd/hlaeja/test/container/PostgresTestContainer.kt new file mode 100644 index 0000000..1e03119 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/test/container/PostgresTestContainer.kt @@ -0,0 +1,15 @@ +package ltd.hlaeja.test.container + +import ltd.hlaeja.test.container.extension.PostgresTestExtension +import ltd.hlaeja.test.container.postgres.PostgresTestListener +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.test.context.ContextConfiguration +import org.springframework.test.context.TestExecutionListeners +import org.springframework.test.context.TestExecutionListeners.MergeMode + +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@ExtendWith(PostgresTestExtension::class) +@ContextConfiguration(initializers = [PostgresTestExtension::class]) +@TestExecutionListeners(listeners = [PostgresTestListener::class], mergeMode = MergeMode.MERGE_WITH_DEFAULTS) +annotation class PostgresTestContainer diff --git a/src/main/kotlin/ltd/hlaeja/test/container/extension/PostgresTestExtension.kt b/src/main/kotlin/ltd/hlaeja/test/container/extension/PostgresTestExtension.kt new file mode 100644 index 0000000..416d113 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/test/container/extension/PostgresTestExtension.kt @@ -0,0 +1,21 @@ +package ltd.hlaeja.test.container.extension + +import ltd.hlaeja.test.container.postgres.TestContainerPostgres +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.springframework.boot.test.util.TestPropertyValues +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext + +class PostgresTestExtension : BeforeAllCallback, ApplicationContextInitializer { + + override fun initialize(applicationContext: ConfigurableApplicationContext) { + TestPropertyValues.of(TestContainerPostgres.props()).applyTo(applicationContext.environment) + } + + override fun beforeAll(context: ExtensionContext) { + if (!TestContainerPostgres.postgres.isRunning) { + TestContainerPostgres.postgres.start() + } + } +} diff --git a/src/main/kotlin/ltd/hlaeja/test/container/postgres/PostgresTestListener.kt b/src/main/kotlin/ltd/hlaeja/test/container/postgres/PostgresTestListener.kt new file mode 100644 index 0000000..8fb030f --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/test/container/postgres/PostgresTestListener.kt @@ -0,0 +1,24 @@ +package ltd.hlaeja.test.container.postgres + +import ltd.hlaeja.test.container.postgres.TestContainerPostgres.sqlFile +import ltd.hlaeja.test.container.util.hasAnnotation +import org.junit.jupiter.api.Nested +import org.springframework.test.context.TestContext +import org.springframework.test.context.TestExecutionListener + +class PostgresTestListener : TestExecutionListener { + + override fun beforeTestClass( + context: TestContext, + ) { + if (context.testClass.hasAnnotation()) return + sqlFile("postgres/data.sql", context) + } + + override fun afterTestClass( + context: TestContext, + ) { + if (context.testClass.hasAnnotation()) return + sqlFile("postgres/reset.sql", context) + } +} diff --git a/src/main/kotlin/ltd/hlaeja/test/container/postgres/TestContainerPostgres.kt b/src/main/kotlin/ltd/hlaeja/test/container/postgres/TestContainerPostgres.kt new file mode 100644 index 0000000..6a0c7b5 --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/test/container/postgres/TestContainerPostgres.kt @@ -0,0 +1,68 @@ +package ltd.hlaeja.test.container.postgres + +import java.io.BufferedReader +import java.io.InputStream +import java.io.InputStreamReader +import kotlinx.coroutines.runBlocking +import ltd.hlaeja.test.container.util.isResourceFile +import org.springframework.r2dbc.core.DatabaseClient +import org.springframework.r2dbc.core.await +import org.springframework.test.context.TestContext +import org.testcontainers.containers.PostgreSQLContainer +import org.testcontainers.utility.DockerImageName + +object TestContainerPostgres { + + val postgres = PostgreSQLContainer(DockerImageName.parse("postgres:17")) + .withReuse(true) + .apply { + withDatabaseName("testdb") + withUsername("test") + withPassword("test") + "postgres/schema.sql".isResourceFile()?.let { withInitScript(it.path) } + } + + fun props(): Map = postgres.let { + mapOf( + "spring.r2dbc.url" to "r2dbc:postgresql://${it.host}:${it.firstMappedPort}/${it.databaseName}", + "spring.r2dbc.username" to it.username, + "spring.r2dbc.password" to it.password, + ) + } + + fun sqlFile( + sqlFile: String, + context: TestContext, + ): Unit = runBlocking { + sqlFile.isResourceFile() + ?.inputStream + ?.use { + executeSqlStatements( + makeSqlStatements(it), + context.applicationContext.getBean(DatabaseClient::class.java), + ) + } + } + + @Suppress("TooGenericExceptionThrown", "SqlSourceToSinkFlow") + private suspend fun executeSqlStatements( + statements: List, + databaseClient: DatabaseClient, + ) = try { + statements.forEach { databaseClient.sql(it).await() } + } catch (e: Exception) { + throw RuntimeException("Failed to execute SQL statements", e) + } + + private fun makeSqlStatements( + inputStream: InputStream, + ): List = BufferedReader(InputStreamReader(inputStream)) + .lines() + .filter { it.isNotEmpty() && !it.startsWith("--") } + .map { it.trim() } + .toList() + .joinToString(" ") + .split(';') + .filter { it.isNotBlank() } + .map { "${it.trim()};" } +} diff --git a/src/main/kotlin/ltd/hlaeja/test/container/util/ContainerUtil.kt b/src/main/kotlin/ltd/hlaeja/test/container/util/ContainerUtil.kt new file mode 100644 index 0000000..9934f2f --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/test/container/util/ContainerUtil.kt @@ -0,0 +1,12 @@ +package ltd.hlaeja.test.container.util + +import org.springframework.core.io.ClassPathResource + +fun String.isResourceFile(): ClassPathResource? = ClassPathResource(this).let { resource -> + when { + resource.exists() && resource.isReadable -> resource + else -> null + } +} + +inline fun Class<*>.hasAnnotation(): Boolean = getAnnotation(T::class.java) != null diff --git a/src/main/kotlin/ltd/hlaeja/test/util/ContainerUtil.kt b/src/main/kotlin/ltd/hlaeja/test/util/ContainerUtil.kt deleted file mode 100644 index 751846f..0000000 --- a/src/main/kotlin/ltd/hlaeja/test/util/ContainerUtil.kt +++ /dev/null @@ -1,26 +0,0 @@ -package ltd.hlaeja.test.util - -import org.springframework.context.ConfigurableApplicationContext -import org.springframework.core.io.ClassPathResource -import org.springframework.test.context.TestContext - -fun ConfigurableApplicationContext.getProperty( - property: String, -): String? = this.environment.getProperty(property) - -fun ConfigurableApplicationContext.getProperty( - property: String, - default: String, -): String = this.environment.getProperty(property, default) - -fun TestContext.getProperty( - property: String, -): String? = this.applicationContext.environment.getProperty(property) - -fun String.isResourceFile(): ClassPathResource? { - val resource = ClassPathResource(this) - return when { - resource.exists() && resource.isReadable -> resource - else -> null - } -}