From f3afc8dfa8de30405a59e01ab0466f808d08d8dc Mon Sep 17 00:00:00 2001 From: mhoennig Date: Wed, 26 Aug 2026 15:21:34 +0200 Subject: [PATCH] refactor(backend): Paketwurzel de.werkbaum, Tests auf RestTestClient Das Backend-Geruest kam unter der Platzhalter-Wurzel com.example.editor herein und widersprach backend/CLAUDE.md. Umgezogen nach de.werkbaum: 17 Kotlin-Dateien, Gradle-group, apiPackage/modelPackage der OpenAPI-Generierung, die jacoco-Ausschluesse und das Cucumber-glue-Paket. Jetzt war es billig, mit jeder Woche Entwicklung waere es teurer geworden. Die BDD-Tests nutzen jetzt RestTestClient statt TestRestTemplate, das in Spring Boot 4 als Auslaufmodell gilt. Weil Cucumber Senden und Pruefen trennt, wird die fluent API nicht fuer Zusicherungen genutzt, sondern ueber returnResult das Ergebnis festgehalten. Nachgemessen: 22 Tests gruen, check inklusive Coverage-Verifikation besteht, 91,7 % Zeilenabdeckung, generierter Code weiterhin ausgeschlossen. Co-Authored-By: Claude Opus 5 --- backend/README.md | 2 +- backend/build.gradle.kts | 15 +- .../werkbaum}/EditorBackendApplication.kt | 2 +- .../werkbaum}/api/DocumentsController.kt | 20 +- .../werkbaum}/api/GlobalExceptionHandler.kt | 6 +- .../editor => de/werkbaum}/domain/Document.kt | 2 +- .../werkbaum}/domain/DocumentHistoryEntry.kt | 2 +- .../werkbaum}/persistence/DocumentEntity.kt | 4 +- .../persistence/DocumentHistoryEntity.kt | 6 +- .../JpaDocumentHistoryRepository.kt | 6 +- .../persistence/JpaDocumentRepository.kt | 6 +- .../persistence/SpringDataRepositories.kt | 2 +- .../repository/DocumentHistoryRepository.kt | 4 +- .../repository/DocumentRepository.kt | 4 +- .../werkbaum}/service/ClockConfiguration.kt | 2 +- .../service/DocumentConflictException.kt | 2 +- .../service/DocumentNotFoundException.kt | 2 +- .../werkbaum}/service/DocumentService.kt | 12 +- .../editor/bdd/DocumentStepDefinitions.kt | 177 ---------------- .../bdd/CucumberSpringConfiguration.kt | 12 +- .../werkbaum}/bdd/CucumberTest.kt | 4 +- .../werkbaum/bdd/DocumentStepDefinitions.kt | 190 ++++++++++++++++++ .../werkbaum}/service/DocumentServiceTest.kt | 12 +- docs/DECISIONS.md | 33 +++ 24 files changed, 286 insertions(+), 241 deletions(-) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/EditorBackendApplication.kt (90%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/api/DocumentsController.kt (82%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/api/GlobalExceptionHandler.kt (86%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/domain/Document.kt (94%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/domain/DocumentHistoryEntry.kt (94%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/persistence/DocumentEntity.kt (93%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/persistence/DocumentHistoryEntity.kt (92%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/persistence/JpaDocumentHistoryRepository.kt (77%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/persistence/JpaDocumentRepository.kt (86%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/persistence/SpringDataRepositories.kt (89%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/repository/DocumentHistoryRepository.kt (73%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/repository/DocumentRepository.kt (84%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/service/ClockConfiguration.kt (86%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/service/DocumentConflictException.kt (69%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/service/DocumentNotFoundException.kt (79%) rename backend/src/main/kotlin/{com/example/editor => de/werkbaum}/service/DocumentService.kt (93%) delete mode 100644 backend/src/test/kotlin/com/example/editor/bdd/DocumentStepDefinitions.kt rename backend/src/test/kotlin/{com/example/editor => de/werkbaum}/bdd/CucumberSpringConfiguration.kt (69%) rename backend/src/test/kotlin/{com/example/editor => de/werkbaum}/bdd/CucumberTest.kt (81%) create mode 100644 backend/src/test/kotlin/de/werkbaum/bdd/DocumentStepDefinitions.kt rename backend/src/test/kotlin/{com/example/editor => de/werkbaum}/service/DocumentServiceTest.kt (96%) diff --git a/backend/README.md b/backend/README.md index cda79fc..5fc5777 100644 --- a/backend/README.md +++ b/backend/README.md @@ -25,7 +25,7 @@ Verschlüsselung. 1. Vertrag ändern: `src/main/resources/openapi/api.yaml` 2. `./gradlew openApiGenerate` → erzeugt `DocumentsApi` (Interface) und Modelle - nach `build/generated/openapi` (Pakete `com.example.editor.generated.*`) + nach `build/generated/openapi` (Pakete `de.werkbaum.generated.*`) 3. `DocumentsController` implementiert das Interface mit `skipDefaultInterface=true`: Weicht die Implementierung vom Vertrag ab, **bricht der Build** – Spezifikation und Code können nicht auseinanderlaufen. diff --git a/backend/build.gradle.kts b/backend/build.gradle.kts index 7589ad2..682afe5 100644 --- a/backend/build.gradle.kts +++ b/backend/build.gradle.kts @@ -8,7 +8,7 @@ plugins { jacoco } -group = "com.example" +group = "de.werkbaum" version = "0.1.0-SNAPSHOT" java { @@ -37,10 +37,9 @@ dependencies { // --- Tests --- testImplementation("org.springframework.boot:spring-boot-starter-test") - // TestRestTemplate liegt in Boot 4 im eigenen Modul (org.springframework.boot.resttestclient) - // und braucht spring-boot-restclient zur Laufzeit + // RestTestClient-Autokonfiguration (Boot 4). TestRestTemplate gilt dort als + // Auslaufmodell; die BDD-Tests nutzen den Nachfolger aus spring-test. testImplementation("org.springframework.boot:spring-boot-resttestclient") - testRuntimeOnly("org.springframework.boot:spring-boot-restclient") testImplementation("io.mockk:mockk:$mockkVersion") // Behavior-Tests (BDD) mit Cucumber @@ -57,8 +56,8 @@ openApiGenerate { generatorName.set("kotlin-spring") inputSpec.set("$projectDir/src/main/resources/openapi/api.yaml") outputDir.set(layout.buildDirectory.dir("generated/openapi").get().asFile.path) - apiPackage.set("com.example.editor.generated.api") - modelPackage.set("com.example.editor.generated.model") + apiPackage.set("de.werkbaum.generated.api") + modelPackage.set("de.werkbaum.generated.model") configOptions.set( mapOf( "useSpringBoot4" to "true", @@ -106,7 +105,7 @@ tasks.jacocoTestReport { // Generierter Code zaehlt nicht zur Coverage classDirectories.setFrom( classDirectories.files.map { - fileTree(it) { exclude("com/example/editor/generated/**") } + fileTree(it) { exclude("de/werkbaum/generated/**") } } ) } @@ -115,7 +114,7 @@ tasks.jacocoTestCoverageVerification { dependsOn(tasks.test) classDirectories.setFrom( classDirectories.files.map { - fileTree(it) { exclude("com/example/editor/generated/**") } + fileTree(it) { exclude("de/werkbaum/generated/**") } } ) violationRules { diff --git a/backend/src/main/kotlin/com/example/editor/EditorBackendApplication.kt b/backend/src/main/kotlin/de/werkbaum/EditorBackendApplication.kt similarity index 90% rename from backend/src/main/kotlin/com/example/editor/EditorBackendApplication.kt rename to backend/src/main/kotlin/de/werkbaum/EditorBackendApplication.kt index 55ab989..c01a40c 100644 --- a/backend/src/main/kotlin/com/example/editor/EditorBackendApplication.kt +++ b/backend/src/main/kotlin/de/werkbaum/EditorBackendApplication.kt @@ -1,4 +1,4 @@ -package com.example.editor +package de.werkbaum import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication diff --git a/backend/src/main/kotlin/com/example/editor/api/DocumentsController.kt b/backend/src/main/kotlin/de/werkbaum/api/DocumentsController.kt similarity index 82% rename from backend/src/main/kotlin/com/example/editor/api/DocumentsController.kt rename to backend/src/main/kotlin/de/werkbaum/api/DocumentsController.kt index e43ee76..aae9a55 100644 --- a/backend/src/main/kotlin/com/example/editor/api/DocumentsController.kt +++ b/backend/src/main/kotlin/de/werkbaum/api/DocumentsController.kt @@ -1,14 +1,14 @@ -package com.example.editor.api +package de.werkbaum.api -import com.example.editor.generated.api.DocumentsApi -import com.example.editor.generated.model.Document as ApiDocument -import com.example.editor.generated.model.DocumentCreateRequest -import com.example.editor.generated.model.DocumentHistoryEntry as ApiHistoryEntry -import com.example.editor.generated.model.DocumentUpdateRequest -import com.example.editor.generated.model.RestoreRequest -import com.example.editor.domain.Document -import com.example.editor.domain.DocumentHistoryEntry -import com.example.editor.service.DocumentService +import de.werkbaum.generated.api.DocumentsApi +import de.werkbaum.generated.model.Document as ApiDocument +import de.werkbaum.generated.model.DocumentCreateRequest +import de.werkbaum.generated.model.DocumentHistoryEntry as ApiHistoryEntry +import de.werkbaum.generated.model.DocumentUpdateRequest +import de.werkbaum.generated.model.RestoreRequest +import de.werkbaum.domain.Document +import de.werkbaum.domain.DocumentHistoryEntry +import de.werkbaum.service.DocumentService import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.RequestMapping diff --git a/backend/src/main/kotlin/com/example/editor/api/GlobalExceptionHandler.kt b/backend/src/main/kotlin/de/werkbaum/api/GlobalExceptionHandler.kt similarity index 86% rename from backend/src/main/kotlin/com/example/editor/api/GlobalExceptionHandler.kt rename to backend/src/main/kotlin/de/werkbaum/api/GlobalExceptionHandler.kt index 21ddf08..0e36490 100644 --- a/backend/src/main/kotlin/com/example/editor/api/GlobalExceptionHandler.kt +++ b/backend/src/main/kotlin/de/werkbaum/api/GlobalExceptionHandler.kt @@ -1,7 +1,7 @@ -package com.example.editor.api +package de.werkbaum.api -import com.example.editor.service.DocumentConflictException -import com.example.editor.service.DocumentNotFoundException +import de.werkbaum.service.DocumentConflictException +import de.werkbaum.service.DocumentNotFoundException import org.springframework.http.HttpStatus import org.springframework.http.ProblemDetail import org.springframework.web.bind.annotation.ExceptionHandler diff --git a/backend/src/main/kotlin/com/example/editor/domain/Document.kt b/backend/src/main/kotlin/de/werkbaum/domain/Document.kt similarity index 94% rename from backend/src/main/kotlin/com/example/editor/domain/Document.kt rename to backend/src/main/kotlin/de/werkbaum/domain/Document.kt index e8514d5..b67cee2 100644 --- a/backend/src/main/kotlin/com/example/editor/domain/Document.kt +++ b/backend/src/main/kotlin/de/werkbaum/domain/Document.kt @@ -1,4 +1,4 @@ -package com.example.editor.domain +package de.werkbaum.domain import java.time.OffsetDateTime import java.util.UUID diff --git a/backend/src/main/kotlin/com/example/editor/domain/DocumentHistoryEntry.kt b/backend/src/main/kotlin/de/werkbaum/domain/DocumentHistoryEntry.kt similarity index 94% rename from backend/src/main/kotlin/com/example/editor/domain/DocumentHistoryEntry.kt rename to backend/src/main/kotlin/de/werkbaum/domain/DocumentHistoryEntry.kt index ac37060..76bfe98 100644 --- a/backend/src/main/kotlin/com/example/editor/domain/DocumentHistoryEntry.kt +++ b/backend/src/main/kotlin/de/werkbaum/domain/DocumentHistoryEntry.kt @@ -1,4 +1,4 @@ -package com.example.editor.domain +package de.werkbaum.domain import java.time.OffsetDateTime import java.util.UUID diff --git a/backend/src/main/kotlin/com/example/editor/persistence/DocumentEntity.kt b/backend/src/main/kotlin/de/werkbaum/persistence/DocumentEntity.kt similarity index 93% rename from backend/src/main/kotlin/com/example/editor/persistence/DocumentEntity.kt rename to backend/src/main/kotlin/de/werkbaum/persistence/DocumentEntity.kt index d51a2e8..00c418e 100644 --- a/backend/src/main/kotlin/com/example/editor/persistence/DocumentEntity.kt +++ b/backend/src/main/kotlin/de/werkbaum/persistence/DocumentEntity.kt @@ -1,6 +1,6 @@ -package com.example.editor.persistence +package de.werkbaum.persistence -import com.example.editor.domain.Document +import de.werkbaum.domain.Document import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.Id diff --git a/backend/src/main/kotlin/com/example/editor/persistence/DocumentHistoryEntity.kt b/backend/src/main/kotlin/de/werkbaum/persistence/DocumentHistoryEntity.kt similarity index 92% rename from backend/src/main/kotlin/com/example/editor/persistence/DocumentHistoryEntity.kt rename to backend/src/main/kotlin/de/werkbaum/persistence/DocumentHistoryEntity.kt index fb277c3..f85a191 100644 --- a/backend/src/main/kotlin/com/example/editor/persistence/DocumentHistoryEntity.kt +++ b/backend/src/main/kotlin/de/werkbaum/persistence/DocumentHistoryEntity.kt @@ -1,7 +1,7 @@ -package com.example.editor.persistence +package de.werkbaum.persistence -import com.example.editor.domain.ChangeType -import com.example.editor.domain.DocumentHistoryEntry +import de.werkbaum.domain.ChangeType +import de.werkbaum.domain.DocumentHistoryEntry import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.EnumType diff --git a/backend/src/main/kotlin/com/example/editor/persistence/JpaDocumentHistoryRepository.kt b/backend/src/main/kotlin/de/werkbaum/persistence/JpaDocumentHistoryRepository.kt similarity index 77% rename from backend/src/main/kotlin/com/example/editor/persistence/JpaDocumentHistoryRepository.kt rename to backend/src/main/kotlin/de/werkbaum/persistence/JpaDocumentHistoryRepository.kt index 9f6556b..38fe4c9 100644 --- a/backend/src/main/kotlin/com/example/editor/persistence/JpaDocumentHistoryRepository.kt +++ b/backend/src/main/kotlin/de/werkbaum/persistence/JpaDocumentHistoryRepository.kt @@ -1,7 +1,7 @@ -package com.example.editor.persistence +package de.werkbaum.persistence -import com.example.editor.domain.DocumentHistoryEntry -import com.example.editor.repository.DocumentHistoryRepository +import de.werkbaum.domain.DocumentHistoryEntry +import de.werkbaum.repository.DocumentHistoryRepository import org.springframework.stereotype.Repository import java.util.UUID diff --git a/backend/src/main/kotlin/com/example/editor/persistence/JpaDocumentRepository.kt b/backend/src/main/kotlin/de/werkbaum/persistence/JpaDocumentRepository.kt similarity index 86% rename from backend/src/main/kotlin/com/example/editor/persistence/JpaDocumentRepository.kt rename to backend/src/main/kotlin/de/werkbaum/persistence/JpaDocumentRepository.kt index c583f41..b85dd16 100644 --- a/backend/src/main/kotlin/com/example/editor/persistence/JpaDocumentRepository.kt +++ b/backend/src/main/kotlin/de/werkbaum/persistence/JpaDocumentRepository.kt @@ -1,7 +1,7 @@ -package com.example.editor.persistence +package de.werkbaum.persistence -import com.example.editor.domain.Document -import com.example.editor.repository.DocumentRepository +import de.werkbaum.domain.Document +import de.werkbaum.repository.DocumentRepository import org.springframework.stereotype.Repository import java.util.UUID diff --git a/backend/src/main/kotlin/com/example/editor/persistence/SpringDataRepositories.kt b/backend/src/main/kotlin/de/werkbaum/persistence/SpringDataRepositories.kt similarity index 89% rename from backend/src/main/kotlin/com/example/editor/persistence/SpringDataRepositories.kt rename to backend/src/main/kotlin/de/werkbaum/persistence/SpringDataRepositories.kt index b0479a1..cd3f240 100644 --- a/backend/src/main/kotlin/com/example/editor/persistence/SpringDataRepositories.kt +++ b/backend/src/main/kotlin/de/werkbaum/persistence/SpringDataRepositories.kt @@ -1,4 +1,4 @@ -package com.example.editor.persistence +package de.werkbaum.persistence import org.springframework.data.jpa.repository.JpaRepository import java.util.UUID diff --git a/backend/src/main/kotlin/com/example/editor/repository/DocumentHistoryRepository.kt b/backend/src/main/kotlin/de/werkbaum/repository/DocumentHistoryRepository.kt similarity index 73% rename from backend/src/main/kotlin/com/example/editor/repository/DocumentHistoryRepository.kt rename to backend/src/main/kotlin/de/werkbaum/repository/DocumentHistoryRepository.kt index 70f4875..df40ad9 100644 --- a/backend/src/main/kotlin/com/example/editor/repository/DocumentHistoryRepository.kt +++ b/backend/src/main/kotlin/de/werkbaum/repository/DocumentHistoryRepository.kt @@ -1,6 +1,6 @@ -package com.example.editor.repository +package de.werkbaum.repository -import com.example.editor.domain.DocumentHistoryEntry +import de.werkbaum.domain.DocumentHistoryEntry import java.util.UUID interface DocumentHistoryRepository { diff --git a/backend/src/main/kotlin/com/example/editor/repository/DocumentRepository.kt b/backend/src/main/kotlin/de/werkbaum/repository/DocumentRepository.kt similarity index 84% rename from backend/src/main/kotlin/com/example/editor/repository/DocumentRepository.kt rename to backend/src/main/kotlin/de/werkbaum/repository/DocumentRepository.kt index e422d1f..9e8434e 100644 --- a/backend/src/main/kotlin/com/example/editor/repository/DocumentRepository.kt +++ b/backend/src/main/kotlin/de/werkbaum/repository/DocumentRepository.kt @@ -1,6 +1,6 @@ -package com.example.editor.repository +package de.werkbaum.repository -import com.example.editor.domain.Document +import de.werkbaum.domain.Document import java.util.UUID /** diff --git a/backend/src/main/kotlin/com/example/editor/service/ClockConfiguration.kt b/backend/src/main/kotlin/de/werkbaum/service/ClockConfiguration.kt similarity index 86% rename from backend/src/main/kotlin/com/example/editor/service/ClockConfiguration.kt rename to backend/src/main/kotlin/de/werkbaum/service/ClockConfiguration.kt index 8c5a4f7..3ad16d0 100644 --- a/backend/src/main/kotlin/com/example/editor/service/ClockConfiguration.kt +++ b/backend/src/main/kotlin/de/werkbaum/service/ClockConfiguration.kt @@ -1,4 +1,4 @@ -package com.example.editor.service +package de.werkbaum.service import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration diff --git a/backend/src/main/kotlin/com/example/editor/service/DocumentConflictException.kt b/backend/src/main/kotlin/de/werkbaum/service/DocumentConflictException.kt similarity index 69% rename from backend/src/main/kotlin/com/example/editor/service/DocumentConflictException.kt rename to backend/src/main/kotlin/de/werkbaum/service/DocumentConflictException.kt index 3a448d8..f82f5a7 100644 --- a/backend/src/main/kotlin/com/example/editor/service/DocumentConflictException.kt +++ b/backend/src/main/kotlin/de/werkbaum/service/DocumentConflictException.kt @@ -1,3 +1,3 @@ -package com.example.editor.service +package de.werkbaum.service class DocumentConflictException(message: String) : RuntimeException(message) diff --git a/backend/src/main/kotlin/com/example/editor/service/DocumentNotFoundException.kt b/backend/src/main/kotlin/de/werkbaum/service/DocumentNotFoundException.kt similarity index 79% rename from backend/src/main/kotlin/com/example/editor/service/DocumentNotFoundException.kt rename to backend/src/main/kotlin/de/werkbaum/service/DocumentNotFoundException.kt index 5dda58b..6f41d15 100644 --- a/backend/src/main/kotlin/com/example/editor/service/DocumentNotFoundException.kt +++ b/backend/src/main/kotlin/de/werkbaum/service/DocumentNotFoundException.kt @@ -1,4 +1,4 @@ -package com.example.editor.service +package de.werkbaum.service import java.util.UUID diff --git a/backend/src/main/kotlin/com/example/editor/service/DocumentService.kt b/backend/src/main/kotlin/de/werkbaum/service/DocumentService.kt similarity index 93% rename from backend/src/main/kotlin/com/example/editor/service/DocumentService.kt rename to backend/src/main/kotlin/de/werkbaum/service/DocumentService.kt index 6c5eede..67606fb 100644 --- a/backend/src/main/kotlin/com/example/editor/service/DocumentService.kt +++ b/backend/src/main/kotlin/de/werkbaum/service/DocumentService.kt @@ -1,10 +1,10 @@ -package com.example.editor.service +package de.werkbaum.service -import com.example.editor.domain.ChangeType -import com.example.editor.domain.Document -import com.example.editor.domain.DocumentHistoryEntry -import com.example.editor.repository.DocumentHistoryRepository -import com.example.editor.repository.DocumentRepository +import de.werkbaum.domain.ChangeType +import de.werkbaum.domain.Document +import de.werkbaum.domain.DocumentHistoryEntry +import de.werkbaum.repository.DocumentHistoryRepository +import de.werkbaum.repository.DocumentRepository import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.Clock diff --git a/backend/src/test/kotlin/com/example/editor/bdd/DocumentStepDefinitions.kt b/backend/src/test/kotlin/com/example/editor/bdd/DocumentStepDefinitions.kt deleted file mode 100644 index 19d10d3..0000000 --- a/backend/src/test/kotlin/com/example/editor/bdd/DocumentStepDefinitions.kt +++ /dev/null @@ -1,177 +0,0 @@ -package com.example.editor.bdd - -import io.cucumber.java.de.Angenommen -import io.cucumber.java.de.Dann -import io.cucumber.java.de.Und -import io.cucumber.java.de.Wenn -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Assertions.assertTrue -import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.resttestclient.TestRestTemplate -import org.springframework.http.HttpEntity -import org.springframework.http.HttpHeaders -import org.springframework.http.HttpMethod -import org.springframework.http.MediaType -import org.springframework.http.ResponseEntity - -/** - * Behavior-Tests gegen die laufende Anwendung (RANDOM_PORT), also echtes - * Verhalten der API inklusive Serialisierung, Statuscodes und Fehlerpfaden. - */ -class DocumentStepDefinitions { - - @Autowired - private lateinit var rest: TestRestTemplate - - private var lastResponse: ResponseEntity? = null - private var currentDocumentId: String? = null - - private fun jsonEntity(body: String): HttpEntity { - val headers = HttpHeaders().apply { contentType = MediaType.APPLICATION_JSON } - return HttpEntity(body, headers) - } - - private fun createDocument(title: String, content: String): ResponseEntity = - rest.postForEntity( - "/api/v1/documents", - jsonEntity("""{"title":"$title","content":"$content"}"""), - String::class.java, - ) - - private fun extractId(body: String?): String { - val match = Regex("\"id\"\\s*:\\s*\"([^\"]+)\"").find(body ?: "") - assertNotNull(match, "Antwort enthält keine ID: $body") - return match!!.groupValues[1] - } - - // ---------------- Angenommen ---------------- - - @Angenommen("es existiert ein Dokument mit dem Titel {string}") - fun `es existiert ein Dokument`(titel: String) { - val response = createDocument(titel, "Initialer Inhalt") - assertEquals(201, response.statusCode.value(), "Testdatenanlage fehlgeschlagen") - currentDocumentId = extractId(response.body) - } - - // ---------------- Wenn ---------------- - - @Wenn("ich ein Dokument mit dem Titel {string} und dem Inhalt {string} anlege") - fun `ich lege ein Dokument an`(titel: String, inhalt: String) { - lastResponse = createDocument(titel, inhalt) - currentDocumentId = Regex("\"id\"\\s*:\\s*\"([^\"]+)\"") - .find(lastResponse?.body ?: "")?.groupValues?.get(1) - } - - @Wenn("ich alle Dokumente abrufe") - fun `ich rufe alle Dokumente ab`() { - lastResponse = rest.getForEntity("/api/v1/documents", String::class.java) - } - - @Wenn("ich dieses Dokument abrufe") - fun `ich rufe dieses Dokument ab`() { - lastResponse = rest.getForEntity("/api/v1/documents/$currentDocumentId", String::class.java) - } - - @Wenn("ich ein Dokument mit einer unbekannten ID abrufe") - fun `ich rufe ein unbekanntes Dokument ab`() { - lastResponse = rest.getForEntity( - "/api/v1/documents/00000000-0000-0000-0000-000000000000", - String::class.java, - ) - } - - @Wenn("ich den Titel dieses Dokuments auf {string} ändere") - fun `ich aendere den Titel`(neuerTitel: String) { - lastResponse = rest.exchange( - "/api/v1/documents/$currentDocumentId", - HttpMethod.PUT, - jsonEntity("""{"title":"$neuerTitel","content":"Aktualisierter Inhalt"}"""), - String::class.java, - ) - } - - @Wenn("ich dieses Dokument lösche") - fun `ich loesche dieses Dokument`() { - lastResponse = rest.exchange( - "/api/v1/documents/$currentDocumentId", - HttpMethod.DELETE, - HttpEntity.EMPTY, - String::class.java, - ) - } - - // ---------------- Dann / Und ---------------- - - @Dann("erhalte ich den Status {int}") - fun `erhalte ich den Status`(status: Int) { - assertEquals(status, lastResponse?.statusCode?.value()) - } - - @Und("die Antwort enthält den Titel {string}") - fun `die Antwort enthaelt den Titel`(titel: String) { - assertTrue( - lastResponse?.body?.contains("\"title\":\"$titel\"") == true, - "Erwarteter Titel '$titel' nicht in Antwort: ${lastResponse?.body}", - ) - } - - @Und("die Antwort enthält {int} Dokumente") - fun `die Antwort enthaelt n Dokumente`(anzahl: Int) { - val count = Regex("\"id\"").findAll(lastResponse?.body ?: "").count() - assertEquals(anzahl, count, "Antwort: ${lastResponse?.body}") - } - - @Und("die Antwort enthält die Version {long}") - fun `die Antwort enthaelt die Version`(version: Long) { - assertTrue( - lastResponse?.body?.contains("\"version\":$version") == true, - "Erwartete Version $version nicht in Antwort: ${lastResponse?.body}", - ) - } - - @Und("das Dokument ist nicht mehr abrufbar") - fun `das Dokument ist nicht mehr abrufbar`() { - val response = rest.getForEntity("/api/v1/documents/$currentDocumentId", String::class.java) - assertEquals(404, response.statusCode.value()) - } - - // ---------------- Historie & Wiederherstellung ---------------- - - @Wenn("ich die Historie dieses Dokuments abrufe") - fun `ich rufe die Historie ab`() { - lastResponse = rest.getForEntity( - "/api/v1/documents/$currentDocumentId/history", - String::class.java, - ) - } - - @Wenn("ich dieses Dokument wiederherstelle") - fun `ich stelle dieses Dokument wieder her`() { - lastResponse = rest.postForEntity( - "/api/v1/documents/$currentDocumentId/restore", - jsonEntity("{}"), - String::class.java, - ) - } - - @Und("die Antwort enthält {int} Historieneinträge") - fun `die Antwort enthaelt n Historieneintraege`(anzahl: Int) { - val count = Regex("\"changeType\"").findAll(lastResponse?.body ?: "").count() - assertEquals(anzahl, count, "Antwort: ${lastResponse?.body}") - } - - @Und("die Antwort enthält den Änderungstyp {string}") - fun `die Antwort enthaelt den Aenderungstyp`(typ: String) { - assertTrue( - lastResponse?.body?.contains("\"changeType\":\"$typ\"") == true, - "Erwarteter Änderungstyp '$typ' nicht in Antwort: ${lastResponse?.body}", - ) - } - - @Und("das Dokument ist wieder abrufbar") - fun `das Dokument ist wieder abrufbar`() { - val response = rest.getForEntity("/api/v1/documents/$currentDocumentId", String::class.java) - assertEquals(200, response.statusCode.value()) - } -} diff --git a/backend/src/test/kotlin/com/example/editor/bdd/CucumberSpringConfiguration.kt b/backend/src/test/kotlin/de/werkbaum/bdd/CucumberSpringConfiguration.kt similarity index 69% rename from backend/src/test/kotlin/com/example/editor/bdd/CucumberSpringConfiguration.kt rename to backend/src/test/kotlin/de/werkbaum/bdd/CucumberSpringConfiguration.kt index 81a4f0f..a714e0a 100644 --- a/backend/src/test/kotlin/com/example/editor/bdd/CucumberSpringConfiguration.kt +++ b/backend/src/test/kotlin/de/werkbaum/bdd/CucumberSpringConfiguration.kt @@ -1,17 +1,17 @@ -package com.example.editor.bdd +package de.werkbaum.bdd -import com.example.editor.repository.DocumentHistoryRepository -import com.example.editor.repository.DocumentRepository +import de.werkbaum.repository.DocumentHistoryRepository +import de.werkbaum.repository.DocumentRepository import io.cucumber.java.Before import io.cucumber.spring.CucumberContextConfiguration import org.springframework.beans.factory.annotation.Autowired -import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate +import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient import org.springframework.boot.test.context.SpringBootTest @CucumberContextConfiguration @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) -// Seit Boot 4 stellt @SpringBootTest die TestRestTemplate-Bean nicht mehr von selbst bereit -@AutoConfigureTestRestTemplate +// Seit Boot 4 stellt @SpringBootTest die Test-Client-Bean nicht mehr von selbst bereit +@AutoConfigureRestTestClient class CucumberSpringConfiguration { @Autowired diff --git a/backend/src/test/kotlin/com/example/editor/bdd/CucumberTest.kt b/backend/src/test/kotlin/de/werkbaum/bdd/CucumberTest.kt similarity index 81% rename from backend/src/test/kotlin/com/example/editor/bdd/CucumberTest.kt rename to backend/src/test/kotlin/de/werkbaum/bdd/CucumberTest.kt index 3fa21aa..3944984 100644 --- a/backend/src/test/kotlin/com/example/editor/bdd/CucumberTest.kt +++ b/backend/src/test/kotlin/de/werkbaum/bdd/CucumberTest.kt @@ -1,4 +1,4 @@ -package com.example.editor.bdd +package de.werkbaum.bdd import io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME import io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME @@ -10,6 +10,6 @@ import org.junit.platform.suite.api.Suite @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") -@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "com.example.editor.bdd") +@ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "de.werkbaum.bdd") @ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "pretty") class CucumberTest diff --git a/backend/src/test/kotlin/de/werkbaum/bdd/DocumentStepDefinitions.kt b/backend/src/test/kotlin/de/werkbaum/bdd/DocumentStepDefinitions.kt new file mode 100644 index 0000000..cc7f74b --- /dev/null +++ b/backend/src/test/kotlin/de/werkbaum/bdd/DocumentStepDefinitions.kt @@ -0,0 +1,190 @@ +package de.werkbaum.bdd + +import io.cucumber.java.de.Angenommen +import io.cucumber.java.de.Dann +import io.cucumber.java.de.Und +import io.cucumber.java.de.Wenn +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.http.MediaType +import org.springframework.test.web.servlet.client.EntityExchangeResult +import org.springframework.test.web.servlet.client.RestTestClient + +/** + * Behavior-Tests gegen die laufende Anwendung (RANDOM_PORT), also echtes + * Verhalten der API inklusive Serialisierung, Statuscodes und Fehlerpfaden. + * + * Verwendet [RestTestClient] – den Nachfolger von TestRestTemplate, das in + * Spring Boot 4 als Auslaufmodell gilt. Die fluent API prueft normalerweise + * sofort; hier wird stattdessen ueber `returnResult` das Ergebnis + * festgehalten, weil Cucumber Senden (Wenn) und Pruefen (Dann) trennt. + */ +class DocumentStepDefinitions { + + @Autowired + private lateinit var client: RestTestClient + + private var lastResponse: EntityExchangeResult? = null + private var currentDocumentId: String? = null + + private fun status(): Int? = lastResponse?.status?.value() + + private fun body(): String? = lastResponse?.responseBody + + private fun createDocument(title: String, content: String): EntityExchangeResult = + client.post() + .uri("/api/v1/documents") + .contentType(MediaType.APPLICATION_JSON) + .body("""{"title":"$title","content":"$content"}""") + .exchange() + .returnResult(String::class.java) + + private fun extractId(body: String?): String { + val match = Regex("\"id\"\\s*:\\s*\"([^\"]+)\"").find(body ?: "") + assertNotNull(match, "Antwort enthält keine ID: $body") + return match!!.groupValues[1] + } + + // ---------------- Angenommen ---------------- + + @Angenommen("es existiert ein Dokument mit dem Titel {string}") + fun `es existiert ein Dokument`(titel: String) { + val response = createDocument(titel, "Initialer Inhalt") + assertEquals(201, response.status.value(), "Testdatenanlage fehlgeschlagen") + currentDocumentId = extractId(response.responseBody) + } + + // ---------------- Wenn ---------------- + + @Wenn("ich ein Dokument mit dem Titel {string} und dem Inhalt {string} anlege") + fun `ich lege ein Dokument an`(titel: String, inhalt: String) { + lastResponse = createDocument(titel, inhalt) + currentDocumentId = Regex("\"id\"\\s*:\\s*\"([^\"]+)\"") + .find(body() ?: "")?.groupValues?.get(1) + } + + @Wenn("ich alle Dokumente abrufe") + fun `ich rufe alle Dokumente ab`() { + lastResponse = client.get() + .uri("/api/v1/documents") + .exchange() + .returnResult(String::class.java) + } + + @Wenn("ich dieses Dokument abrufe") + fun `ich rufe dieses Dokument ab`() { + lastResponse = client.get() + .uri("/api/v1/documents/$currentDocumentId") + .exchange() + .returnResult(String::class.java) + } + + @Wenn("ich ein Dokument mit einer unbekannten ID abrufe") + fun `ich rufe ein unbekanntes Dokument ab`() { + lastResponse = client.get() + .uri("/api/v1/documents/00000000-0000-0000-0000-000000000000") + .exchange() + .returnResult(String::class.java) + } + + @Wenn("ich den Titel dieses Dokuments auf {string} ändere") + fun `ich aendere den Titel`(neuerTitel: String) { + lastResponse = client.put() + .uri("/api/v1/documents/$currentDocumentId") + .contentType(MediaType.APPLICATION_JSON) + .body("""{"title":"$neuerTitel","content":"Aktualisierter Inhalt"}""") + .exchange() + .returnResult(String::class.java) + } + + @Wenn("ich dieses Dokument lösche") + fun `ich loesche dieses Dokument`() { + lastResponse = client.delete() + .uri("/api/v1/documents/$currentDocumentId") + .exchange() + .returnResult(String::class.java) + } + + // ---------------- Dann / Und ---------------- + + @Dann("erhalte ich den Status {int}") + fun `erhalte ich den Status`(status: Int) { + assertEquals(status, status()) + } + + @Und("die Antwort enthält den Titel {string}") + fun `die Antwort enthaelt den Titel`(titel: String) { + assertTrue( + body()?.contains("\"title\":\"$titel\"") == true, + "Erwarteter Titel '$titel' nicht in Antwort: ${body()}", + ) + } + + @Und("die Antwort enthält {int} Dokumente") + fun `die Antwort enthaelt n Dokumente`(anzahl: Int) { + val count = Regex("\"id\"").findAll(body() ?: "").count() + assertEquals(anzahl, count, "Antwort: ${body()}") + } + + @Und("die Antwort enthält die Version {long}") + fun `die Antwort enthaelt die Version`(version: Long) { + assertTrue( + body()?.contains("\"version\":$version") == true, + "Erwartete Version $version nicht in Antwort: ${body()}", + ) + } + + @Und("das Dokument ist nicht mehr abrufbar") + fun `das Dokument ist nicht mehr abrufbar`() { + val response = client.get() + .uri("/api/v1/documents/$currentDocumentId") + .exchange() + .returnResult(String::class.java) + assertEquals(404, response.status.value()) + } + + // ---------------- Historie & Wiederherstellung ---------------- + + @Wenn("ich die Historie dieses Dokuments abrufe") + fun `ich rufe die Historie ab`() { + lastResponse = client.get() + .uri("/api/v1/documents/$currentDocumentId/history") + .exchange() + .returnResult(String::class.java) + } + + @Wenn("ich dieses Dokument wiederherstelle") + fun `ich stelle dieses Dokument wieder her`() { + lastResponse = client.post() + .uri("/api/v1/documents/$currentDocumentId/restore") + .contentType(MediaType.APPLICATION_JSON) + .body("{}") + .exchange() + .returnResult(String::class.java) + } + + @Und("die Antwort enthält {int} Historieneinträge") + fun `die Antwort enthaelt n Historieneintraege`(anzahl: Int) { + val count = Regex("\"changeType\"").findAll(body() ?: "").count() + assertEquals(anzahl, count, "Antwort: ${body()}") + } + + @Und("die Antwort enthält den Änderungstyp {string}") + fun `die Antwort enthaelt den Aenderungstyp`(typ: String) { + assertTrue( + body()?.contains("\"changeType\":\"$typ\"") == true, + "Erwarteter Änderungstyp '$typ' nicht in Antwort: ${body()}", + ) + } + + @Und("das Dokument ist wieder abrufbar") + fun `das Dokument ist wieder abrufbar`() { + val response = client.get() + .uri("/api/v1/documents/$currentDocumentId") + .exchange() + .returnResult(String::class.java) + assertEquals(200, response.status.value()) + } +} diff --git a/backend/src/test/kotlin/com/example/editor/service/DocumentServiceTest.kt b/backend/src/test/kotlin/de/werkbaum/service/DocumentServiceTest.kt similarity index 96% rename from backend/src/test/kotlin/com/example/editor/service/DocumentServiceTest.kt rename to backend/src/test/kotlin/de/werkbaum/service/DocumentServiceTest.kt index 85364c1..33bc849 100644 --- a/backend/src/test/kotlin/com/example/editor/service/DocumentServiceTest.kt +++ b/backend/src/test/kotlin/de/werkbaum/service/DocumentServiceTest.kt @@ -1,10 +1,10 @@ -package com.example.editor.service +package de.werkbaum.service -import com.example.editor.domain.ChangeType -import com.example.editor.domain.Document -import com.example.editor.domain.DocumentHistoryEntry -import com.example.editor.repository.DocumentHistoryRepository -import com.example.editor.repository.DocumentRepository +import de.werkbaum.domain.ChangeType +import de.werkbaum.domain.Document +import de.werkbaum.domain.DocumentHistoryEntry +import de.werkbaum.repository.DocumentHistoryRepository +import de.werkbaum.repository.DocumentRepository import io.mockk.every import io.mockk.just import io.mockk.mockk diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 216044a..c2f2982 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -5967,3 +5967,36 @@ und JDBC sind JVM-Bibliotheken; das wäre kein Umbau, sondern ein Neubau auf einem anderen Stack. Unabhängig davon: Beim Deployment gehört ein `-Xmx` gesetzt, statt der JVM auf einem geteilten Server die Voreinstellung zu überlassen. + +**Nachtrag zu D13 — Paketwurzel `de.werkbaum`, und drei Fallen von Spring +Boot 4 (2026-08-26).** Das Backend-Gerüst kam zunächst unter der +Platzhalter-Wurzel `com.example.editor` herein und widersprach damit +`backend/CLAUDE.md`. Umgezogen nach **`de.werkbaum`** (17 Kotlin-Dateien, +Gradle-`group`, `apiPackage`/`modelPackage` der OpenAPI-Generierung, die +jacoco-Ausschlüsse und das Cucumber-`glue`-Paket) — jetzt war es billig, mit +jeder Woche Entwicklung wäre es teurer geworden. Nachgemessen: 22 Tests grün, +`check` inklusive Coverage-Verifikation besteht, 91,7 % Zeilenabdeckung, +generierter Code weiterhin ausgeschlossen. + +Dabei sind drei Eigenheiten von **Spring Boot 4** aufgefallen, die jeweils +denselben Ursprung haben — die Modularisierung, bei der vieles aus dem +Kern-Artefakt in eigene Module gewandert ist: + +1. **`TestRestTemplate` ist umgezogen** von `org.springframework.boot.test.web.client` + nach `org.springframework.boot.resttestclient` und liegt im Modul + `spring-boot-resttestclient`, das `spring-boot-starter-test` **nicht** + mitbringt. +2. **`@SpringBootTest` stellt die Test-Client-Bean nicht mehr von selbst + bereit** — es braucht `@AutoConfigureTestRestTemplate` bzw. + `@AutoConfigureRestTestClient`. +3. **Liquibase braucht seinen Starter.** Das nackte `org.liquibase:liquibase-core` + bringt die Autokonfiguration nicht mehr mit; ohne + `spring-boot-starter-liquibase` läuft keine Migration, und die Tests + scheitern erst spät mit „Schema validation: missing table". + +Die BDD-Tests nutzen jetzt **`RestTestClient`** (aus `spring-test`) statt +`TestRestTemplate`, das in Boot 4 als Auslaufmodell gilt. Umgestellt bei +fünfzehn Aufrufen in einer Datei — die Zahl wächst von hier an nur. Weil +Cucumber Senden (Wenn) und Prüfen (Dann) trennt, wird die fluent API nicht +für Zusicherungen genutzt, sondern über `returnResult` das Ergebnis +festgehalten.