test(backend): Kotest-Assertions statt JUnit, CLAUDE.md berichtigt
backend/CLAUDE.md sah Kotest-Assertions vor, der Code benutzte aber die JUnit-Assertions. Umgestellt bei 25 Aufrufen in zwei Dateien - jetzt billig, und Test-Abhaengigkeiten sind unkritisch, weil sie in keinem Artefakt landen. Wo Kotest spezifischer ist, ersetzt es die handgeschriebenen Meldungen: shouldContain statt assertTrue(contains, "..."), withClue nur noch dort, wo der Kontext wirklich hilft. Gegenprobe per Mutation: eine falsche Erwartung im Service-Test und eine im BDD-Schritt lassen genau die danach benannten Tests fallen. CLAUDE.md berichtigt: Das Backend ist nicht mehr "noch nicht bootstrapped", die Konfiguration heisst application.yaml, die Schichten entsprechen dem tatsaechlichen Aufbau, und die drei Spring-Boot-4-Fallen von heute sind festgehalten, damit sie niemanden erneut treffen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
f3afc8dfa8
commit
b6509ae537
+33
-8
@@ -1,17 +1,42 @@
|
|||||||
# Werkbaum · Backend
|
# Werkbaum · Backend
|
||||||
|
|
||||||
Kotlin/Spring-Boot-Anwendung. Aufgaben: Persistenz der Notationstexte,
|
Kotlin/Spring-Boot-Anwendung. Aufgaben: Persistenz der Notationstexte,
|
||||||
Taiga-Integration (REST-API, `#ref`-Auflösung, Status-Sync), später
|
Live-Editing (D76), Taiga-Integration (REST-API, `#ref`-Auflösung,
|
||||||
Tenzu-Adapter. Noch nicht bootstrapped — siehe README.md hier.
|
Status-Sync), später Tenzu-Adapter.
|
||||||
|
|
||||||
|
**Stand:** Gerüst steht — Dokumenten-CRUD mit Historie und Wiederherstellung,
|
||||||
|
API-First aus `src/main/resources/openapi/api.yaml`, H2 mit Liquibase.
|
||||||
|
Kommandos in README.md hier. Live-Editing ist entschieden (D76), aber noch
|
||||||
|
nicht gebaut: `docs/live-editing-proposal.md`.
|
||||||
|
|
||||||
## Konventionen
|
## Konventionen
|
||||||
- Kotlin, Spring Boot, Gradle (Kotlin DSL), JDK 21.
|
- Kotlin, **Spring Boot 4**, Gradle (Kotlin DSL), JDK 21.
|
||||||
- Paketwurzel `de.werkbaum`; Schichten: `api` (Controller/DTOs),
|
- Paketwurzel `de.werkbaum`; Schichten: `api` (Controller), `domain`,
|
||||||
`domain`, `integration.taiga` (Client, Mapping), `persistence`.
|
`service`, `repository` (Interfaces), `persistence` (JPA), später
|
||||||
- Tests mit JUnit 5 + Kotest-Assertions; Taiga-Client gegen
|
`integration.taiga` (Client, Mapping).
|
||||||
aufgezeichnete Antworten (WireMock), nie gegen Live-Instanzen.
|
- **API First:** Interfaces und Modelle werden aus der OpenAPI-Spezifikation
|
||||||
- Konfiguration über `application.yml` + Umgebungsvariablen;
|
generiert; der Controller implementiert sie. Ändert sich die Spec, schlägt
|
||||||
|
der Compile fehl — genau so ist es gewollt.
|
||||||
|
- Tests mit JUnit 5 als Runner + **Kotest-Assertions** (`shouldBe`,
|
||||||
|
`shouldContain`, `shouldThrow`) und MockK; Verhalten per Cucumber gegen die
|
||||||
|
laufende Anwendung (`RestTestClient`, nicht TestRestTemplate — das ist in
|
||||||
|
Boot 4 Auslaufmodell). Taiga-Client gegen aufgezeichnete Antworten
|
||||||
|
(WireMock), nie gegen Live-Instanzen.
|
||||||
|
- Konfiguration über `application.yaml` + Umgebungsvariablen;
|
||||||
keine Zugangsdaten im Repository.
|
keine Zugangsdaten im Repository.
|
||||||
|
- Keine neuen **Laufzeit**-Abhängigkeiten ohne Rückfrage (Wurzel-CLAUDE.md);
|
||||||
|
Test-Abhängigkeiten sind unkritisch, sie landen in keinem Artefakt.
|
||||||
|
|
||||||
|
## Spring Boot 4 — drei Fallen (D13-Nachtrag)
|
||||||
|
Vieles ist aus dem Kern in eigene Module gewandert. Was uns getroffen hat:
|
||||||
|
|
||||||
|
- `spring-boot-starter-test` bringt **kein** `TestRestTemplate`/`RestTestClient`
|
||||||
|
mit — dafür `spring-boot-resttestclient`.
|
||||||
|
- `@SpringBootTest` stellt die Test-Client-Bean **nicht** mehr von selbst
|
||||||
|
bereit: `@AutoConfigureRestTestClient` gehört an die Testkonfiguration.
|
||||||
|
- `org.liquibase:liquibase-core` allein bringt die Autokonfiguration nicht
|
||||||
|
mehr mit; ohne `spring-boot-starter-liquibase` läuft keine Migration, und
|
||||||
|
der Fehler zeigt sich erst spät als „Schema validation: missing table".
|
||||||
|
|
||||||
## Wichtig (D14 — Parser-Hoheit)
|
## Wichtig (D14 — Parser-Hoheit)
|
||||||
Das Backend parst die Notation **nicht**. Es speichert den Text als Ganzes
|
Das Backend parst die Notation **nicht**. Es speichert den Text als Ganzes
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ repositories {
|
|||||||
|
|
||||||
val cucumberVersion = "7.23.0"
|
val cucumberVersion = "7.23.0"
|
||||||
val mockkVersion = "1.13.16"
|
val mockkVersion = "1.13.16"
|
||||||
|
val kotestVersion = "5.9.1"
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("org.springframework.boot:spring-boot-starter-web")
|
implementation("org.springframework.boot:spring-boot-starter-web")
|
||||||
@@ -41,6 +42,8 @@ dependencies {
|
|||||||
// Auslaufmodell; die BDD-Tests nutzen den Nachfolger aus spring-test.
|
// Auslaufmodell; die BDD-Tests nutzen den Nachfolger aus spring-test.
|
||||||
testImplementation("org.springframework.boot:spring-boot-resttestclient")
|
testImplementation("org.springframework.boot:spring-boot-resttestclient")
|
||||||
testImplementation("io.mockk:mockk:$mockkVersion")
|
testImplementation("io.mockk:mockk:$mockkVersion")
|
||||||
|
// Nur die Assertions - Testrunner bleibt JUnit 5 (backend/CLAUDE.md)
|
||||||
|
testImplementation("io.kotest:kotest-assertions-core:$kotestVersion")
|
||||||
|
|
||||||
// Behavior-Tests (BDD) mit Cucumber
|
// Behavior-Tests (BDD) mit Cucumber
|
||||||
testImplementation("io.cucumber:cucumber-java:$cucumberVersion")
|
testImplementation("io.cucumber:cucumber-java:$cucumberVersion")
|
||||||
|
|||||||
@@ -4,9 +4,10 @@ import io.cucumber.java.de.Angenommen
|
|||||||
import io.cucumber.java.de.Dann
|
import io.cucumber.java.de.Dann
|
||||||
import io.cucumber.java.de.Und
|
import io.cucumber.java.de.Und
|
||||||
import io.cucumber.java.de.Wenn
|
import io.cucumber.java.de.Wenn
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
import io.kotest.assertions.withClue
|
||||||
import org.junit.jupiter.api.Assertions.assertNotNull
|
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||||
import org.junit.jupiter.api.Assertions.assertTrue
|
import io.kotest.matchers.shouldBe
|
||||||
|
import io.kotest.matchers.string.shouldContain
|
||||||
import org.springframework.beans.factory.annotation.Autowired
|
import org.springframework.beans.factory.annotation.Autowired
|
||||||
import org.springframework.http.MediaType
|
import org.springframework.http.MediaType
|
||||||
import org.springframework.test.web.servlet.client.EntityExchangeResult
|
import org.springframework.test.web.servlet.client.EntityExchangeResult
|
||||||
@@ -31,7 +32,7 @@ class DocumentStepDefinitions {
|
|||||||
|
|
||||||
private fun status(): Int? = lastResponse?.status?.value()
|
private fun status(): Int? = lastResponse?.status?.value()
|
||||||
|
|
||||||
private fun body(): String? = lastResponse?.responseBody
|
private fun body(): String = lastResponse?.responseBody.orEmpty()
|
||||||
|
|
||||||
private fun createDocument(title: String, content: String): EntityExchangeResult<String> =
|
private fun createDocument(title: String, content: String): EntityExchangeResult<String> =
|
||||||
client.post()
|
client.post()
|
||||||
@@ -41,18 +42,17 @@ class DocumentStepDefinitions {
|
|||||||
.exchange()
|
.exchange()
|
||||||
.returnResult(String::class.java)
|
.returnResult(String::class.java)
|
||||||
|
|
||||||
private fun extractId(body: String?): String {
|
private fun extractId(body: String?): String =
|
||||||
val match = Regex("\"id\"\\s*:\\s*\"([^\"]+)\"").find(body ?: "")
|
withClue("Antwort enthält keine ID: $body") {
|
||||||
assertNotNull(match, "Antwort enthält keine ID: $body")
|
Regex("\"id\"\\s*:\\s*\"([^\"]+)\"").find(body ?: "").shouldNotBeNull()
|
||||||
return match!!.groupValues[1]
|
}.groupValues[1]
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------- Angenommen ----------------
|
// ---------------- Angenommen ----------------
|
||||||
|
|
||||||
@Angenommen("es existiert ein Dokument mit dem Titel {string}")
|
@Angenommen("es existiert ein Dokument mit dem Titel {string}")
|
||||||
fun `es existiert ein Dokument`(titel: String) {
|
fun `es existiert ein Dokument`(titel: String) {
|
||||||
val response = createDocument(titel, "Initialer Inhalt")
|
val response = createDocument(titel, "Initialer Inhalt")
|
||||||
assertEquals(201, response.status.value(), "Testdatenanlage fehlgeschlagen")
|
withClue("Testdatenanlage fehlgeschlagen") { response.status.value() shouldBe 201 }
|
||||||
currentDocumentId = extractId(response.responseBody)
|
currentDocumentId = extractId(response.responseBody)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ class DocumentStepDefinitions {
|
|||||||
fun `ich lege ein Dokument an`(titel: String, inhalt: String) {
|
fun `ich lege ein Dokument an`(titel: String, inhalt: String) {
|
||||||
lastResponse = createDocument(titel, inhalt)
|
lastResponse = createDocument(titel, inhalt)
|
||||||
currentDocumentId = Regex("\"id\"\\s*:\\s*\"([^\"]+)\"")
|
currentDocumentId = Regex("\"id\"\\s*:\\s*\"([^\"]+)\"")
|
||||||
.find(body() ?: "")?.groupValues?.get(1)
|
.find(body())?.groupValues?.get(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Wenn("ich alle Dokumente abrufe")
|
@Wenn("ich alle Dokumente abrufe")
|
||||||
@@ -110,30 +110,24 @@ class DocumentStepDefinitions {
|
|||||||
// ---------------- Dann / Und ----------------
|
// ---------------- Dann / Und ----------------
|
||||||
|
|
||||||
@Dann("erhalte ich den Status {int}")
|
@Dann("erhalte ich den Status {int}")
|
||||||
fun `erhalte ich den Status`(status: Int) {
|
fun `erhalte ich den Status`(erwartet: Int) {
|
||||||
assertEquals(status, status())
|
status() shouldBe erwartet
|
||||||
}
|
}
|
||||||
|
|
||||||
@Und("die Antwort enthält den Titel {string}")
|
@Und("die Antwort enthält den Titel {string}")
|
||||||
fun `die Antwort enthaelt den Titel`(titel: String) {
|
fun `die Antwort enthaelt den Titel`(titel: String) {
|
||||||
assertTrue(
|
body() shouldContain "\"title\":\"$titel\""
|
||||||
body()?.contains("\"title\":\"$titel\"") == true,
|
|
||||||
"Erwarteter Titel '$titel' nicht in Antwort: ${body()}",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Und("die Antwort enthält {int} Dokumente")
|
@Und("die Antwort enthält {int} Dokumente")
|
||||||
fun `die Antwort enthaelt n Dokumente`(anzahl: Int) {
|
fun `die Antwort enthaelt n Dokumente`(anzahl: Int) {
|
||||||
val count = Regex("\"id\"").findAll(body() ?: "").count()
|
val count = Regex("\"id\"").findAll(body()).count()
|
||||||
assertEquals(anzahl, count, "Antwort: ${body()}")
|
withClue("Antwort: ${body()}") { count shouldBe anzahl }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Und("die Antwort enthält die Version {long}")
|
@Und("die Antwort enthält die Version {long}")
|
||||||
fun `die Antwort enthaelt die Version`(version: Long) {
|
fun `die Antwort enthaelt die Version`(version: Long) {
|
||||||
assertTrue(
|
body() shouldContain "\"version\":$version"
|
||||||
body()?.contains("\"version\":$version") == true,
|
|
||||||
"Erwartete Version $version nicht in Antwort: ${body()}",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Und("das Dokument ist nicht mehr abrufbar")
|
@Und("das Dokument ist nicht mehr abrufbar")
|
||||||
@@ -142,7 +136,7 @@ class DocumentStepDefinitions {
|
|||||||
.uri("/api/v1/documents/$currentDocumentId")
|
.uri("/api/v1/documents/$currentDocumentId")
|
||||||
.exchange()
|
.exchange()
|
||||||
.returnResult(String::class.java)
|
.returnResult(String::class.java)
|
||||||
assertEquals(404, response.status.value())
|
response.status.value() shouldBe 404
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------- Historie & Wiederherstellung ----------------
|
// ---------------- Historie & Wiederherstellung ----------------
|
||||||
@@ -167,16 +161,13 @@ class DocumentStepDefinitions {
|
|||||||
|
|
||||||
@Und("die Antwort enthält {int} Historieneinträge")
|
@Und("die Antwort enthält {int} Historieneinträge")
|
||||||
fun `die Antwort enthaelt n Historieneintraege`(anzahl: Int) {
|
fun `die Antwort enthaelt n Historieneintraege`(anzahl: Int) {
|
||||||
val count = Regex("\"changeType\"").findAll(body() ?: "").count()
|
val count = Regex("\"changeType\"").findAll(body()).count()
|
||||||
assertEquals(anzahl, count, "Antwort: ${body()}")
|
withClue("Antwort: ${body()}") { count shouldBe anzahl }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Und("die Antwort enthält den Änderungstyp {string}")
|
@Und("die Antwort enthält den Änderungstyp {string}")
|
||||||
fun `die Antwort enthaelt den Aenderungstyp`(typ: String) {
|
fun `die Antwort enthaelt den Aenderungstyp`(typ: String) {
|
||||||
assertTrue(
|
body() shouldContain "\"changeType\":\"$typ\""
|
||||||
body()?.contains("\"changeType\":\"$typ\"") == true,
|
|
||||||
"Erwarteter Änderungstyp '$typ' nicht in Antwort: ${body()}",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Und("das Dokument ist wieder abrufbar")
|
@Und("das Dokument ist wieder abrufbar")
|
||||||
@@ -185,6 +176,6 @@ class DocumentStepDefinitions {
|
|||||||
.uri("/api/v1/documents/$currentDocumentId")
|
.uri("/api/v1/documents/$currentDocumentId")
|
||||||
.exchange()
|
.exchange()
|
||||||
.returnResult(String::class.java)
|
.returnResult(String::class.java)
|
||||||
assertEquals(200, response.status.value())
|
response.status.value() shouldBe 200
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import de.werkbaum.domain.Document
|
|||||||
import de.werkbaum.domain.DocumentHistoryEntry
|
import de.werkbaum.domain.DocumentHistoryEntry
|
||||||
import de.werkbaum.repository.DocumentHistoryRepository
|
import de.werkbaum.repository.DocumentHistoryRepository
|
||||||
import de.werkbaum.repository.DocumentRepository
|
import de.werkbaum.repository.DocumentRepository
|
||||||
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
|
import io.kotest.matchers.shouldBe
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.just
|
import io.mockk.just
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import io.mockk.runs
|
import io.mockk.runs
|
||||||
import io.mockk.slot
|
import io.mockk.slot
|
||||||
import io.mockk.verify
|
import io.mockk.verify
|
||||||
import org.junit.jupiter.api.Assertions.assertEquals
|
|
||||||
import org.junit.jupiter.api.Assertions.assertThrows
|
|
||||||
import org.junit.jupiter.api.Test
|
import org.junit.jupiter.api.Test
|
||||||
import java.time.Clock
|
import java.time.Clock
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -65,10 +65,10 @@ class DocumentServiceTest {
|
|||||||
|
|
||||||
val result = service.create(title = "Notizen", content = "Hallo")
|
val result = service.create(title = "Notizen", content = "Hallo")
|
||||||
|
|
||||||
assertEquals(1, result.version)
|
result.version shouldBe 1
|
||||||
assertEquals(ChangeType.CREATED, historyEntry.captured.changeType)
|
historyEntry.captured.changeType shouldBe ChangeType.CREATED
|
||||||
assertEquals(result.id, historyEntry.captured.documentId)
|
historyEntry.captured.documentId shouldBe result.id
|
||||||
assertEquals("Hallo", historyEntry.captured.content)
|
historyEntry.captured.content shouldBe "Hallo"
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -82,9 +82,9 @@ class DocumentServiceTest {
|
|||||||
|
|
||||||
val result = service.update(doc.id, title = "Neu", content = "Neuer Inhalt")
|
val result = service.update(doc.id, title = "Neu", content = "Neuer Inhalt")
|
||||||
|
|
||||||
assertEquals(4, result.version)
|
result.version shouldBe 4
|
||||||
assertEquals(ChangeType.UPDATED, historyEntry.captured.changeType)
|
historyEntry.captured.changeType shouldBe ChangeType.UPDATED
|
||||||
assertEquals(4, historyEntry.captured.version)
|
historyEntry.captured.version shouldBe 4
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -98,8 +98,8 @@ class DocumentServiceTest {
|
|||||||
service.delete(doc.id)
|
service.delete(doc.id)
|
||||||
|
|
||||||
verify(exactly = 1) { repository.deleteById(doc.id) }
|
verify(exactly = 1) { repository.deleteById(doc.id) }
|
||||||
assertEquals(ChangeType.DELETED, historyEntry.captured.changeType)
|
historyEntry.captured.changeType shouldBe ChangeType.DELETED
|
||||||
assertEquals(3, historyEntry.captured.version)
|
historyEntry.captured.version shouldBe 3
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -107,7 +107,7 @@ class DocumentServiceTest {
|
|||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
every { repository.findById(id) } returns null
|
every { repository.findById(id) } returns null
|
||||||
|
|
||||||
assertThrows(DocumentNotFoundException::class.java) { service.delete(id) }
|
shouldThrow<DocumentNotFoundException> { service.delete(id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -119,7 +119,7 @@ class DocumentServiceTest {
|
|||||||
)
|
)
|
||||||
every { historyRepository.findByDocumentId(id) } returns entries
|
every { historyRepository.findByDocumentId(id) } returns entries
|
||||||
|
|
||||||
assertEquals(entries, service.history(id))
|
service.history(id) shouldBe entries
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -127,7 +127,7 @@ class DocumentServiceTest {
|
|||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
every { historyRepository.findByDocumentId(id) } returns emptyList()
|
every { historyRepository.findByDocumentId(id) } returns emptyList()
|
||||||
|
|
||||||
assertThrows(DocumentNotFoundException::class.java) { service.history(id) }
|
shouldThrow<DocumentNotFoundException> { service.history(id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -146,11 +146,11 @@ class DocumentServiceTest {
|
|||||||
|
|
||||||
val result = service.restore(id)
|
val result = service.restore(id)
|
||||||
|
|
||||||
assertEquals(id, result.id)
|
result.id shouldBe id
|
||||||
assertEquals("Titel v2", result.title)
|
result.title shouldBe "Titel v2"
|
||||||
assertEquals("Inhalt v2", result.content)
|
result.content shouldBe "Inhalt v2"
|
||||||
assertEquals(4, result.version)
|
result.version shouldBe 4
|
||||||
assertEquals(ChangeType.RESTORED, historyEntry.captured.changeType)
|
historyEntry.captured.changeType shouldBe ChangeType.RESTORED
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -169,8 +169,8 @@ class DocumentServiceTest {
|
|||||||
|
|
||||||
val result = service.restore(id, targetVersion = 1)
|
val result = service.restore(id, targetVersion = 1)
|
||||||
|
|
||||||
assertEquals("Titel v1", result.title)
|
result.title shouldBe "Titel v1"
|
||||||
assertEquals(4, result.version)
|
result.version shouldBe 4
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -181,7 +181,7 @@ class DocumentServiceTest {
|
|||||||
)
|
)
|
||||||
every { repository.findById(id) } returns sampleDocument(id = id)
|
every { repository.findById(id) } returns sampleDocument(id = id)
|
||||||
|
|
||||||
assertThrows(DocumentConflictException::class.java) { service.restore(id) }
|
shouldThrow<DocumentConflictException> { service.restore(id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -189,7 +189,7 @@ class DocumentServiceTest {
|
|||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
every { historyRepository.findByDocumentId(id) } returns emptyList()
|
every { historyRepository.findByDocumentId(id) } returns emptyList()
|
||||||
|
|
||||||
assertThrows(DocumentNotFoundException::class.java) { service.restore(id) }
|
shouldThrow<DocumentNotFoundException> { service.restore(id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -197,7 +197,7 @@ class DocumentServiceTest {
|
|||||||
val id = UUID.randomUUID()
|
val id = UUID.randomUUID()
|
||||||
every { repository.findById(id) } returns null
|
every { repository.findById(id) } returns null
|
||||||
|
|
||||||
assertThrows(DocumentNotFoundException::class.java) { service.findById(id) }
|
shouldThrow<DocumentNotFoundException> { service.findById(id) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -205,6 +205,6 @@ class DocumentServiceTest {
|
|||||||
val docs = listOf(sampleDocument(), sampleDocument())
|
val docs = listOf(sampleDocument(), sampleDocument())
|
||||||
every { repository.findAll() } returns docs
|
every { repository.findAll() } returns docs
|
||||||
|
|
||||||
assertEquals(docs, service.findAll())
|
service.findAll() shouldBe docs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5994,6 +5994,14 @@ Kern-Artefakt in eigene Module gewandert ist:
|
|||||||
`spring-boot-starter-liquibase` läuft keine Migration, und die Tests
|
`spring-boot-starter-liquibase` läuft keine Migration, und die Tests
|
||||||
scheitern erst spät mit „Schema validation: missing table".
|
scheitern erst spät mit „Schema validation: missing table".
|
||||||
|
|
||||||
|
Die Zusicherungen sind auf **Kotest** umgestellt (`shouldBe`,
|
||||||
|
`shouldContain`, `shouldThrow`) — `backend/CLAUDE.md` hatte das vorgesehen,
|
||||||
|
der Code benutzte aber JUnit-Assertions. Bei 25 Aufrufen war es billig, und
|
||||||
|
Test-Abhängigkeiten sind unkritisch, weil sie in keinem Artefakt landen.
|
||||||
|
Gegenprobe per Mutation: eine falsche Erwartung im Service-Test und eine im
|
||||||
|
BDD-Schritt lassen genau die danach benannten Tests fallen — die
|
||||||
|
Zusicherungen greifen also, statt nur gut auszusehen.
|
||||||
|
|
||||||
Die BDD-Tests nutzen jetzt **`RestTestClient`** (aus `spring-test`) statt
|
Die BDD-Tests nutzen jetzt **`RestTestClient`** (aus `spring-test`) statt
|
||||||
`TestRestTemplate`, das in Boot 4 als Auslaufmodell gilt. Umgestellt bei
|
`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
|
fünfzehn Aufrufen in einer Datei — die Zahl wächst von hier an nur. Weil
|
||||||
|
|||||||
Reference in New Issue
Block a user