Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b129528c35 | |||
| 6aad7e3d63 | |||
| 902a2a2c0b | |||
| be08c0c369 |
14
README.md
14
README.md
@@ -2,18 +2,22 @@
|
||||
|
||||
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/<test path>/resources/postgres` folder.
|
||||
script located in `src/<test path>/resources` folder. And specify properties in `src/<test path>/resources/application.properties`.
|
||||
|
||||
* `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.
|
||||
### Properties For Test Container
|
||||
|
||||
if file exist it will be loaded...
|
||||
| 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 |
|
||||
|
||||
## Releasing library
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ plugins {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(hlaeja.kotlin.logging)
|
||||
implementation(hlaeja.kotlinx.coroutines)
|
||||
implementation(hlaeja.springboot.starter.r2dbc)
|
||||
implementation(hlaeja.springboot.starter.test)
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
kotlin.code.style=official
|
||||
version=0.1.0
|
||||
catalog=0.9.0
|
||||
version=0.2.0
|
||||
catalog=0.10.0
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package ltd.hlaeja.test
|
||||
|
||||
import java.util.UUID
|
||||
import org.assertj.core.api.AbstractAssert
|
||||
import org.assertj.core.api.AbstractStringAssert
|
||||
|
||||
@Suppress("unused")
|
||||
@@ -13,3 +15,12 @@ fun <SELF : AbstractStringAssert<SELF>?> AbstractStringAssert<SELF>.compareToFil
|
||||
?: throw UnsupportedOperationException(
|
||||
"Attempted to compare assertion object to context of a file but expected file was not found: $file",
|
||||
)
|
||||
|
||||
@Suppress("unused")
|
||||
fun <SELF : AbstractAssert<SELF, ACTUAL>?, ACTUAL> AbstractAssert<SELF, ACTUAL>.isEqualToUuid(
|
||||
uuid: String,
|
||||
): SELF = try {
|
||||
UUID.fromString(uuid).let { expected -> this.isEqualTo(expected) }
|
||||
} catch (e: IllegalArgumentException) {
|
||||
throw UnsupportedOperationException("Invalid UUID string provided: $uuid", e)
|
||||
}
|
||||
|
||||
@@ -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