update postgres test container
- update README.md - update PostgresContainer - add TestExecutionListeners - remove ExtendWith - update PostgresInitializer - cleanup - use properties for script and container - add afterTestClass - add beforeTestClass - extend TestExecutionListener - remove PostgresExtension - add debug logging to PostgresExecutor - add ContainerUtils - add dependencies - extract function from PostgresExtension to PostgresExecutor
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
package ltd.hlaeja.test.container
|
||||
|
||||
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.MERGE_WITH_DEFAULTS
|
||||
|
||||
@Suppress("unused")
|
||||
@Retention(AnnotationRetention.RUNTIME)
|
||||
@Target(AnnotationTarget.CLASS)
|
||||
@ExtendWith(PostgresExtension::class)
|
||||
@ContextConfiguration(initializers = [PostgresInitializer::class])
|
||||
@TestExecutionListeners(listeners = [PostgresInitializer::class], mergeMode = MERGE_WITH_DEFAULTS)
|
||||
annotation class PostgresContainer
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
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<String>,
|
||||
) = 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<String> = BufferedReader(InputStreamReader(inputStream))
|
||||
.lines()
|
||||
.filter { it.isNotEmpty() && !it.startsWith("--") }
|
||||
.map { it.trim() }
|
||||
.toList()
|
||||
.joinToString(" ")
|
||||
.split(';')
|
||||
.filter { it.isNotBlank() }
|
||||
.map { "${it.trim()};" }
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
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<String>,
|
||||
) {
|
||||
try {
|
||||
statements.forEach { statement ->
|
||||
connection.createStatement(statement)
|
||||
.execute()
|
||||
.awaitFirstOrNull()
|
||||
}
|
||||
} finally {
|
||||
connection.close().awaitFirstOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeSqlStatements(
|
||||
classPathResource: ClassPathResource,
|
||||
): List<String> = 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()};" }
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,67 @@
|
||||
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.core.io.ClassPathResource
|
||||
import org.springframework.test.context.TestContext
|
||||
import org.springframework.test.context.TestExecutionListener
|
||||
import org.testcontainers.containers.PostgreSQLContainer
|
||||
|
||||
@Suppress("unused")
|
||||
class PostgresInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||
private val log = KotlinLogging.logger {}
|
||||
|
||||
override fun initialize(applicationContext: ConfigurableApplicationContext) {
|
||||
postgres().apply {
|
||||
@Suppress("unused")
|
||||
class PostgresInitializer : ApplicationContextInitializer<ConfigurableApplicationContext>, 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(applicationContext)
|
||||
).applyTo(context)
|
||||
}
|
||||
}
|
||||
|
||||
private fun postgres(): PostgreSQLContainer<*> = PostgreSQLContainer("postgres:17")
|
||||
.withReuse(true)
|
||||
.apply {
|
||||
"postgres/schema.sql".let {
|
||||
if (ClassPathResource(it).exists()) {
|
||||
withInitScript(it)
|
||||
}
|
||||
}
|
||||
start()
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
26
src/main/kotlin/ltd/hlaeja/test/util/ContainerUtil.kt
Normal file
26
src/main/kotlin/ltd/hlaeja/test/util/ContainerUtil.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user