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() + } +}