Skip to content

Commit

Permalink
general updates
Browse files Browse the repository at this point in the history
  • Loading branch information
slu-it committed Apr 11, 2024
1 parent 64a1771 commit f6dd48e
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 149 deletions.
20 changes: 8 additions & 12 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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")
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,5 +100,4 @@ internal class BookRepositoryTest {
private fun BookEntity.changeTitle(): BookEntity =
apply { book = book.copy(title = Title("Change Title #${nextInt(1_000)}")) }
}

}
2 changes: 1 addition & 1 deletion examples/data-mongodb/README.md
Original file line number Diff line number Diff line change
@@ -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.
23 changes: 11 additions & 12 deletions examples/data-mongodb/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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")
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
18 changes: 0 additions & 18 deletions examples/data-mongodb/src/test/resources/application-test.yml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<ConfigurableApplicationContext> {

// 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<AfterTestClassEvent> {
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())
}
}

0 comments on commit f6dd48e

Please sign in to comment.