diff --git a/build.gradle.kts b/build.gradle.kts index fc6c1f6..3b41243 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,14 +6,14 @@ import org.gradle.api.tasks.testing.logging.TestLogEvent.SKIPPED import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { - id("org.springframework.boot") version "3.2.2" apply false + id("org.springframework.boot") version "3.2.4" apply false id("io.spring.dependency-management") version "1.1.4" apply false id("org.asciidoctor.jvm.convert") version "3.3.2" apply false - kotlin("jvm") version "1.9.22" apply false - kotlin("plugin.spring") version "1.9.22" apply false - kotlin("plugin.jpa") version "1.9.22" apply false - kotlin("plugin.noarg") version "1.9.22" apply false + kotlin("jvm") version "1.9.23" apply false + kotlin("plugin.spring") version "1.9.23" apply false + kotlin("plugin.jpa") version "1.9.23" apply false + kotlin("plugin.noarg") version "1.9.23" apply false } allprojects { @@ -29,22 +29,18 @@ allprojects { imports { mavenBom("io.github.logrecorder:logrecorder-bom:2.9.1") mavenBom("io.github.openfeign:feign-bom:13.1") - mavenBom("org.jetbrains.kotlin:kotlin-bom:1.9.22") + mavenBom("org.jetbrains.kotlin:kotlin-bom:1.9.23") mavenBom("org.testcontainers:testcontainers-bom:1.19.3") mavenBom("org.zalando:logbook-bom:3.7.2") - mavenBom("org.springframework.cloud:spring-cloud-dependencies:2023.0.0") + mavenBom("org.springframework.cloud:spring-cloud-dependencies:2023.0.1") mavenBom(org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES) } dependencies { dependency("com.github.dasniko:testcontainers-keycloak:2.6.0") dependency("com.ninja-squad:springmockk:4.0.2") dependency("io.kotest:kotest-assertions-core:5.8.0") - dependency("io.mockk:mockk-jvm:1.13.8") - - // legacy compatibility - dependency("de.flapdoodle.embed:de.flapdoodle.embed.mongo.spring30x:4.11.0") - dependency("org.apache.activemq:artemis-jms-server:2.31.2") + dependency("io.mockk:mockk-jvm:1.13.10") } } } diff --git a/examples/data-jpa/src/test/kotlin/example/spring/boot/data/jpa/persistence/BookRepositoryTest.kt b/examples/data-jpa/src/test/kotlin/example/spring/boot/data/jpa/persistence/BookRepositoryTest.kt index d62dbc7..02fe63d 100644 --- a/examples/data-jpa/src/test/kotlin/example/spring/boot/data/jpa/persistence/BookRepositoryTest.kt +++ b/examples/data-jpa/src/test/kotlin/example/spring/boot/data/jpa/persistence/BookRepositoryTest.kt @@ -100,5 +100,4 @@ internal class BookRepositoryTest { private fun BookEntity.changeTitle(): BookEntity = apply { book = book.copy(title = Title("Change Title #${nextInt(1_000)}")) } } - } diff --git a/examples/data-mongodb/README.md b/examples/data-mongodb/README.md index 455463d..6b02641 100644 --- a/examples/data-mongodb/README.md +++ b/examples/data-mongodb/README.md @@ -1,4 +1,4 @@ # Spring Boot: Data MongoDB Showcase demonstrating how Data MongoDB repositories can be tested with the help of JUnit 5, Spring -Boot's `@DataMongoTest` support and either an embedded or an actual dockerized MongoDB database. +Boot's `@DataMongoTest` support and a dockerized MongoDB database. diff --git a/examples/data-mongodb/build.gradle.kts b/examples/data-mongodb/build.gradle.kts index e68c00c..ec7aefa 100644 --- a/examples/data-mongodb/build.gradle.kts +++ b/examples/data-mongodb/build.gradle.kts @@ -1,19 +1,18 @@ plugins { - id("org.springframework.boot") - id("io.spring.dependency-management") + id("org.springframework.boot") + id("io.spring.dependency-management") - kotlin("jvm") - kotlin("plugin.spring") + kotlin("jvm") + kotlin("plugin.spring") } dependencies { - implementation("org.springframework.boot:spring-boot-starter-data-mongodb") - implementation("org.springframework.boot:spring-boot-starter-web") - implementation("com.fasterxml.jackson.module:jackson-module-kotlin") - implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") - implementation("org.jetbrains.kotlin:kotlin-reflect") + implementation("org.springframework.boot:spring-boot-starter-data-mongodb") + implementation("org.springframework.boot:spring-boot-starter-web") + implementation("com.fasterxml.jackson.module:jackson-module-kotlin") + implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") + implementation("org.jetbrains.kotlin:kotlin-reflect") - testImplementation("org.springframework.boot:spring-boot-starter-test") - testImplementation("de.flapdoodle.embed:de.flapdoodle.embed.mongo.spring30x") - testImplementation("org.testcontainers:mongodb") + testImplementation("org.springframework.boot:spring-boot-starter-test") + testImplementation("org.testcontainers:mongodb") } diff --git a/examples/data-mongodb/src/test/kotlin/example/spring/boot/data/mongodb/persistence/BookRepositoryTest.kt b/examples/data-mongodb/src/test/kotlin/example/spring/boot/data/mongodb/persistence/BookRepositoryTest.kt index e7147d1..b990173 100644 --- a/examples/data-mongodb/src/test/kotlin/example/spring/boot/data/mongodb/persistence/BookRepositoryTest.kt +++ b/examples/data-mongodb/src/test/kotlin/example/spring/boot/data/mongodb/persistence/BookRepositoryTest.kt @@ -7,96 +7,70 @@ import example.spring.boot.data.mongodb.model.Title import example.spring.boot.data.mongodb.utils.InitializeWithContainerizedMongoDB import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThatThrownBy -import org.junit.jupiter.api.Nested import org.junit.jupiter.api.Test import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest import org.springframework.context.annotation.Import import org.springframework.dao.OptimisticLockingFailureException -import org.springframework.test.context.ActiveProfiles import java.util.UUID.randomUUID -internal class BookRepositoryTest { - - /** - * Much faster boostrap after first download, but pollutes local file system. - */ - @Nested - @DataMongoTest - @ActiveProfiles("test", "embedded") - inner class WithEmbeddedDatabase( - @Autowired override val cut: BookRepository - ) : BookRepositoryContract() - - /** - * Takes longer to boostrap, but also provides real MongoDB behaviour. - */ - @Nested - @DataMongoTest - @ActiveProfiles("test", "docker") - @InitializeWithContainerizedMongoDB - inner class WithDockerizedDatabase( - @Autowired override val cut: BookRepository - ) : BookRepositoryContract() - - @Import(CustomMongoDbConfiguration::class) - abstract class BookRepositoryContract { - - protected abstract val cut: BookRepository - - @Test - fun `document can be saved`() { - val document = bookRecordDocument() - val savedDocument = cut.save(document) - assertThat(savedDocument).isEqualTo(document.copy(version = 1)) - } - - @Test - fun `document version is increased with every save`() { - val document = bookRecordDocument() - val savedDocument1 = cut.save(document) - val savedDocument2 = cut.save(savedDocument1) - val savedDocument3 = cut.save(savedDocument2) - - assertThat(savedDocument1.version).isEqualTo(1) - assertThat(savedDocument2.version).isEqualTo(2) - assertThat(savedDocument3.version).isEqualTo(3) - } +@DataMongoTest +@InitializeWithContainerizedMongoDB +@Import(CustomMongoDbConfiguration::class) +internal class BookRepositoryTest( + @Autowired val cut: BookRepository +) { + + @Test + fun `document can be saved`() { + val document = bookRecordDocument() + val savedDocument = cut.save(document) + assertThat(savedDocument).isEqualTo(document.copy(version = 1)) + } - @Test - fun `document can not be saved in lower than current version`() { - val document = bookRecordDocument() - .let(cut::save) - .let(cut::save) - val documentWithLowerVersion = document.copy(version = document.version - 1) + @Test + fun `document version is increased with every save`() { + val document = bookRecordDocument() + val savedDocument1 = cut.save(document) + val savedDocument2 = cut.save(savedDocument1) + val savedDocument3 = cut.save(savedDocument2) - assertThatThrownBy { cut.save(documentWithLowerVersion) } - .isInstanceOf(OptimisticLockingFailureException::class.java) - } + assertThat(savedDocument1.version).isEqualTo(1) + assertThat(savedDocument2.version).isEqualTo(2) + assertThat(savedDocument3.version).isEqualTo(3) + } - @Test - fun `document can be found by id`() { - val savedDocument = cut.save(bookRecordDocument()) - val foundDocument = cut.findById(savedDocument.id) - assertThat(foundDocument).hasValue(savedDocument) - } + @Test + fun `document can not be saved in lower than current version`() { + val document = bookRecordDocument() + .let(cut::save) + .let(cut::save) + val documentWithLowerVersion = document.copy(version = document.version - 1) - @Test - fun `document can be found by title`() { - val d1 = cut.save(bookRecordDocument("Clean Code")) - val d2 = cut.save(bookRecordDocument("Clean Architecture")) - val d3 = cut.save(bookRecordDocument("Clean Code")) + assertThatThrownBy { cut.save(documentWithLowerVersion) } + .isInstanceOf(OptimisticLockingFailureException::class.java) + } - val foundEntities = cut.findByTitle("Clean Code") + @Test + fun `document can be found by id`() { + val savedDocument = cut.save(bookRecordDocument()) + val foundDocument = cut.findById(savedDocument.id) + assertThat(foundDocument).hasValue(savedDocument) + } - assertThat(foundEntities) - .contains(d1, d3) - .doesNotContain(d2) - } + @Test + fun `document can be found by title`() { + val d1 = cut.save(bookRecordDocument("Clean Code")) + val d2 = cut.save(bookRecordDocument("Clean Architecture")) + val d3 = cut.save(bookRecordDocument("Clean Code")) - private fun bookRecordDocument(title: String = "Clean Code") = - BookDocument(randomUUID(), Book(Isbn("9780123456789"), Title(title))) + val foundEntities = cut.findByTitle("Clean Code") + assertThat(foundEntities) + .contains(d1, d3) + .doesNotContain(d2) } + private fun bookRecordDocument(title: String = "Clean Code") = + BookDocument(randomUUID(), Book(Isbn("9780123456789"), Title(title))) } diff --git a/examples/data-mongodb/src/test/resources/application-test.yml b/examples/data-mongodb/src/test/resources/application-test.yml deleted file mode 100644 index ca9c6f0..0000000 --- a/examples/data-mongodb/src/test/resources/application-test.yml +++ /dev/null @@ -1,18 +0,0 @@ ---- - -spring.config.activate.on-profile: "embedded" - -de: - flapdoodle: - mongodb: - embedded: - version: "4.0.12" - ---- - -spring.config.activate.on-profile: "docker" - -spring: - autoconfigure: - exclude: - - de.flapdoodle.embed.mongo.spring.autoconfigure.EmbeddedMongoAutoConfiguration diff --git a/examples/messaging-rabbitmq/src/test/kotlin/example/spring/boot/rabbitmq/utils/InitializeWithContainerizedRabbitMQ.kt b/examples/messaging-rabbitmq/src/test/kotlin/example/spring/boot/rabbitmq/utils/InitializeWithContainerizedRabbitMQ.kt index 3aa3d11..6cf9c98 100644 --- a/examples/messaging-rabbitmq/src/test/kotlin/example/spring/boot/rabbitmq/utils/InitializeWithContainerizedRabbitMQ.kt +++ b/examples/messaging-rabbitmq/src/test/kotlin/example/spring/boot/rabbitmq/utils/InitializeWithContainerizedRabbitMQ.kt @@ -1,53 +1,63 @@ package example.spring.boot.rabbitmq.utils import org.springframework.context.ApplicationContextInitializer -import org.springframework.context.ApplicationListener import org.springframework.context.ConfigurableApplicationContext -import org.springframework.test.annotation.DirtiesContext -import org.springframework.test.annotation.DirtiesContext.ClassMode.AFTER_CLASS import org.springframework.test.context.ContextConfiguration -import org.springframework.test.context.event.AfterTestClassEvent import org.springframework.test.context.support.TestPropertySourceUtils.addInlinedPropertiesToEnvironment import org.testcontainers.containers.RabbitMQContainer +import java.net.Authenticator +import java.net.PasswordAuthentication +import java.net.URI.create +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpRequest.BodyPublishers.noBody +import java.net.http.HttpResponse.BodyHandlers.discarding +import java.util.UUID.randomUUID import kotlin.annotation.AnnotationTarget.CLASS @Retention @Target(CLASS) -@DirtiesContext(classMode = AFTER_CLASS) @ContextConfiguration(initializers = [RabbitMQInitializer::class]) annotation class InitializeWithContainerizedRabbitMQ class RabbitMQInitializer : ApplicationContextInitializer { - // Unlike other initializers of this kind (e.g. our PostgreSQL and MongoDB examples) RabbitMQ does not have - // anything like separated databases, namespaces or other easy to access / configure mechanisms for isolating - // test (classes) from each other. - - // That is why we are using a new container for each test application context. - - // To safe on resources the test application contexts should be stopped after each test class in order for the - // running container to be stopped as soon as possible. (see @DirtiesContext for how to do that) - - // Alternatives to this approach might be: - // - Use a single container like in the other examples and make sure that each test uses new random topics and - // queues to manually isolate the test from each other. - // - Find some way to drop all queues and topics of the broker programmatically after each test (class). + companion object { + private val container: RabbitMQContainer by lazy { + RabbitMQContainer("rabbitmq:3.13-management") + .apply { start() } + } + private val httpClient: HttpClient by lazy { + HttpClient.newBuilder() + .authenticator(BasicAuthenticator(container.adminUsername, container.adminPassword)) + .build() + } + } override fun initialize(applicationContext: ConfigurableApplicationContext) { - val container: RabbitMQContainer = RabbitMQContainer("rabbitmq:3.11") - .apply { start() } - - val listener = StopContainerListener(container) - applicationContext.addApplicationListener(listener) - - val hostProperty = "spring.rabbitmq.host=${container.host}" - val portProperty = "spring.rabbitmq.port=${container.amqpPort}" - - addInlinedPropertiesToEnvironment(applicationContext, hostProperty, portProperty) + val virtualHost = createVirtualHost() + + addInlinedPropertiesToEnvironment( + applicationContext, + "spring.rabbitmq.host=${container.host}", + "spring.rabbitmq.port=${container.amqpPort}", + "spring.rabbitmq.username=${container.adminUsername}", + "spring.rabbitmq.password=${container.adminPassword}", + "spring.rabbitmq.virtual-host=$virtualHost", + ) } - class StopContainerListener(private val container: RabbitMQContainer) : ApplicationListener { - override fun onApplicationEvent(event: AfterTestClassEvent) = container.stop() + private fun createVirtualHost(): String { + val virtualHost = "${randomUUID()}" + val request = HttpRequest.newBuilder() + .PUT(noBody()) + .uri(create("http://${container.host}:${container.httpPort}/api/vhosts/$virtualHost")) + .build() + httpClient.send(request, discarding()) + return virtualHost } + private class BasicAuthenticator(val username: String, val password: String) : Authenticator() { + override fun getPasswordAuthentication() = PasswordAuthentication(username, password.toCharArray()) + } }