From 1f6329f68741e6560ac66e24d455063b9e86f961 Mon Sep 17 00:00:00 2001 From: Swordsteel Date: Thu, 30 Jan 2025 15:12:25 +0100 Subject: [PATCH] Set up postgresql testcontainers - add .json to .editorconfig - add spring-configuration-metadata.json - update README.md - add testcontainers dependencies - add PostgresContainer - add PostgresInitializer --- README.md | 13 ++++ build.gradle.kts | 4 + .../test/container/PostgresContainer.kt | 11 +++ .../test/container/PostgresExtension.kt | 74 +++++++++++++++++++ .../test/container/PostgresInitializer.kt | 32 ++++++++ 5 files changed, 134 insertions(+) create mode 100644 src/main/kotlin/ltd/hlaeja/test/container/PostgresContainer.kt create mode 100644 src/main/kotlin/ltd/hlaeja/test/container/PostgresExtension.kt create mode 100644 src/main/kotlin/ltd/hlaeja/test/container/PostgresInitializer.kt diff --git a/README.md b/README.md index 224ac2f..a47dbc4 100644 --- a/README.md +++ b/README.md @@ -2,6 +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. + +Initialize Postgres test container using spring properties for R2DBC, +script located in `src//resources/postgres` folder. + +* `schema.sql` file containing all structure and functions when star. +* `data.sql` file containing all data added before all test. +* `reset.sql` file containing all to reset database after all test. + +if file exist it will be loaded... + ## Releasing library Run `release.sh` script from `master` branch. diff --git a/build.gradle.kts b/build.gradle.kts index 7762b49..9ec1a5a 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,11 @@ plugins { } dependencies { + implementation(hlaeja.kotlinx.coroutines) + implementation(hlaeja.springboot.starter.r2dbc) implementation(hlaeja.springboot.starter.test) + implementation(hlaeja.testcontainers.junit) + implementation(hlaeja.testcontainers.postgresql) testRuntimeOnly(hlaeja.junit.platform.launcher) } diff --git a/src/main/kotlin/ltd/hlaeja/test/container/PostgresContainer.kt b/src/main/kotlin/ltd/hlaeja/test/container/PostgresContainer.kt new file mode 100644 index 0000000..251896b --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/test/container/PostgresContainer.kt @@ -0,0 +1,11 @@ +package ltd.hlaeja.test.container + +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.test.context.ContextConfiguration + +@Suppress("unused") +@Retention(AnnotationRetention.RUNTIME) +@Target(AnnotationTarget.CLASS) +@ExtendWith(PostgresExtension::class) +@ContextConfiguration(initializers = [PostgresInitializer::class]) +annotation class PostgresContainer diff --git a/src/main/kotlin/ltd/hlaeja/test/container/PostgresExtension.kt b/src/main/kotlin/ltd/hlaeja/test/container/PostgresExtension.kt new file mode 100644 index 0000000..d01268b --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/test/container/PostgresExtension.kt @@ -0,0 +1,74 @@ +package ltd.hlaeja.test.container + +import java.io.BufferedReader +import java.io.InputStreamReader +import org.junit.jupiter.api.extension.AfterAllCallback +import org.junit.jupiter.api.extension.BeforeAllCallback +import org.junit.jupiter.api.extension.ExtensionContext +import org.springframework.core.io.ClassPathResource +import org.springframework.test.context.junit.jupiter.SpringExtension +import kotlinx.coroutines.runBlocking +import io.r2dbc.spi.Connection +import io.r2dbc.spi.ConnectionFactory +import kotlinx.coroutines.reactive.awaitFirstOrElse +import kotlinx.coroutines.reactive.awaitFirstOrNull + +class PostgresExtension : BeforeAllCallback, AfterAllCallback { + + override fun beforeAll(context: ExtensionContext) { + executeSqlFile(context, "postgres/data.sql") + } + + override fun afterAll(context: ExtensionContext) { + executeSqlFile(context, "postgres/reset.sql") + } + + private fun executeSqlFile( + context: ExtensionContext, + resourceSourcePath: String, + ) = runBlocking { + if (ClassPathResource(resourceSourcePath).exists()) { + executeSqlStatements( + postgresConnection(context), + makeSqlStatements(ClassPathResource(resourceSourcePath)), + ) + } + } + + @Suppress("TooGenericExceptionThrown") + private suspend fun postgresConnection( + context: ExtensionContext, + ) = SpringExtension.getApplicationContext(context) + .getBean(ConnectionFactory::class.java) + .create() + .awaitFirstOrElse { throw RuntimeException("Connection factory could not be created") } + + private suspend fun executeSqlStatements( + connection: Connection, + statements: List, + ) { + try { + statements.forEach { statement -> + connection.createStatement(statement) + .execute() + .awaitFirstOrNull() + } + } finally { + connection.close().awaitFirstOrNull() + } + } + + private fun makeSqlStatements( + classPathResource: ClassPathResource, + ): List = classPathResource.inputStream.use { inputStream -> + 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 new file mode 100644 index 0000000..e12923e --- /dev/null +++ b/src/main/kotlin/ltd/hlaeja/test/container/PostgresInitializer.kt @@ -0,0 +1,32 @@ +package ltd.hlaeja.test.container + +import org.springframework.boot.test.util.TestPropertyValues +import org.springframework.context.ApplicationContextInitializer +import org.springframework.context.ConfigurableApplicationContext +import org.springframework.core.io.ClassPathResource +import org.testcontainers.containers.PostgreSQLContainer + +@Suppress("unused") +class PostgresInitializer : ApplicationContextInitializer { + + override fun initialize(applicationContext: ConfigurableApplicationContext) { + postgres().apply { + TestPropertyValues.of( + "spring.r2dbc.url=r2dbc:pool:postgresql://$host:$firstMappedPort/$databaseName", + "spring.r2dbc.username=$username", + "spring.r2dbc.password=$password", + ).applyTo(applicationContext) + } + } + + private fun postgres(): PostgreSQLContainer<*> = PostgreSQLContainer("postgres:17") + .withReuse(true) + .apply { + "postgres/schema.sql".let { + if (ClassPathResource(it).exists()) { + withInitScript(it) + } + } + start() + } +}