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
@@ -4,9 +4,10 @@ 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 io.kotest.assertions.withClue
|
||||
import io.kotest.matchers.nulls.shouldNotBeNull
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.test.web.servlet.client.EntityExchangeResult
|
||||
@@ -31,7 +32,7 @@ class DocumentStepDefinitions {
|
||||
|
||||
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> =
|
||||
client.post()
|
||||
@@ -41,18 +42,17 @@ class DocumentStepDefinitions {
|
||||
.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]
|
||||
}
|
||||
private fun extractId(body: String?): String =
|
||||
withClue("Antwort enthält keine ID: $body") {
|
||||
Regex("\"id\"\\s*:\\s*\"([^\"]+)\"").find(body ?: "").shouldNotBeNull()
|
||||
}.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")
|
||||
withClue("Testdatenanlage fehlgeschlagen") { response.status.value() shouldBe 201 }
|
||||
currentDocumentId = extractId(response.responseBody)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ class DocumentStepDefinitions {
|
||||
fun `ich lege ein Dokument an`(titel: String, inhalt: String) {
|
||||
lastResponse = createDocument(titel, inhalt)
|
||||
currentDocumentId = Regex("\"id\"\\s*:\\s*\"([^\"]+)\"")
|
||||
.find(body() ?: "")?.groupValues?.get(1)
|
||||
.find(body())?.groupValues?.get(1)
|
||||
}
|
||||
|
||||
@Wenn("ich alle Dokumente abrufe")
|
||||
@@ -110,30 +110,24 @@ class DocumentStepDefinitions {
|
||||
// ---------------- Dann / Und ----------------
|
||||
|
||||
@Dann("erhalte ich den Status {int}")
|
||||
fun `erhalte ich den Status`(status: Int) {
|
||||
assertEquals(status, status())
|
||||
fun `erhalte ich den Status`(erwartet: Int) {
|
||||
status() shouldBe erwartet
|
||||
}
|
||||
|
||||
@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()}",
|
||||
)
|
||||
body() shouldContain "\"title\":\"$titel\""
|
||||
}
|
||||
|
||||
@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()}")
|
||||
val count = Regex("\"id\"").findAll(body()).count()
|
||||
withClue("Antwort: ${body()}") { count shouldBe anzahl }
|
||||
}
|
||||
|
||||
@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()}",
|
||||
)
|
||||
body() shouldContain "\"version\":$version"
|
||||
}
|
||||
|
||||
@Und("das Dokument ist nicht mehr abrufbar")
|
||||
@@ -142,7 +136,7 @@ class DocumentStepDefinitions {
|
||||
.uri("/api/v1/documents/$currentDocumentId")
|
||||
.exchange()
|
||||
.returnResult(String::class.java)
|
||||
assertEquals(404, response.status.value())
|
||||
response.status.value() shouldBe 404
|
||||
}
|
||||
|
||||
// ---------------- Historie & Wiederherstellung ----------------
|
||||
@@ -167,16 +161,13 @@ class DocumentStepDefinitions {
|
||||
|
||||
@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()}")
|
||||
val count = Regex("\"changeType\"").findAll(body()).count()
|
||||
withClue("Antwort: ${body()}") { count shouldBe anzahl }
|
||||
}
|
||||
|
||||
@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()}",
|
||||
)
|
||||
body() shouldContain "\"changeType\":\"$typ\""
|
||||
}
|
||||
|
||||
@Und("das Dokument ist wieder abrufbar")
|
||||
@@ -185,6 +176,6 @@ class DocumentStepDefinitions {
|
||||
.uri("/api/v1/documents/$currentDocumentId")
|
||||
.exchange()
|
||||
.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.repository.DocumentHistoryRepository
|
||||
import de.werkbaum.repository.DocumentRepository
|
||||
import io.kotest.assertions.throwables.shouldThrow
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.mockk
|
||||
import io.mockk.runs
|
||||
import io.mockk.slot
|
||||
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 java.time.Clock
|
||||
import java.time.Instant
|
||||
@@ -65,10 +65,10 @@ class DocumentServiceTest {
|
||||
|
||||
val result = service.create(title = "Notizen", content = "Hallo")
|
||||
|
||||
assertEquals(1, result.version)
|
||||
assertEquals(ChangeType.CREATED, historyEntry.captured.changeType)
|
||||
assertEquals(result.id, historyEntry.captured.documentId)
|
||||
assertEquals("Hallo", historyEntry.captured.content)
|
||||
result.version shouldBe 1
|
||||
historyEntry.captured.changeType shouldBe ChangeType.CREATED
|
||||
historyEntry.captured.documentId shouldBe result.id
|
||||
historyEntry.captured.content shouldBe "Hallo"
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -82,9 +82,9 @@ class DocumentServiceTest {
|
||||
|
||||
val result = service.update(doc.id, title = "Neu", content = "Neuer Inhalt")
|
||||
|
||||
assertEquals(4, result.version)
|
||||
assertEquals(ChangeType.UPDATED, historyEntry.captured.changeType)
|
||||
assertEquals(4, historyEntry.captured.version)
|
||||
result.version shouldBe 4
|
||||
historyEntry.captured.changeType shouldBe ChangeType.UPDATED
|
||||
historyEntry.captured.version shouldBe 4
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -98,8 +98,8 @@ class DocumentServiceTest {
|
||||
service.delete(doc.id)
|
||||
|
||||
verify(exactly = 1) { repository.deleteById(doc.id) }
|
||||
assertEquals(ChangeType.DELETED, historyEntry.captured.changeType)
|
||||
assertEquals(3, historyEntry.captured.version)
|
||||
historyEntry.captured.changeType shouldBe ChangeType.DELETED
|
||||
historyEntry.captured.version shouldBe 3
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -107,7 +107,7 @@ class DocumentServiceTest {
|
||||
val id = UUID.randomUUID()
|
||||
every { repository.findById(id) } returns null
|
||||
|
||||
assertThrows(DocumentNotFoundException::class.java) { service.delete(id) }
|
||||
shouldThrow<DocumentNotFoundException> { service.delete(id) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -119,7 +119,7 @@ class DocumentServiceTest {
|
||||
)
|
||||
every { historyRepository.findByDocumentId(id) } returns entries
|
||||
|
||||
assertEquals(entries, service.history(id))
|
||||
service.history(id) shouldBe entries
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -127,7 +127,7 @@ class DocumentServiceTest {
|
||||
val id = UUID.randomUUID()
|
||||
every { historyRepository.findByDocumentId(id) } returns emptyList()
|
||||
|
||||
assertThrows(DocumentNotFoundException::class.java) { service.history(id) }
|
||||
shouldThrow<DocumentNotFoundException> { service.history(id) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -146,11 +146,11 @@ class DocumentServiceTest {
|
||||
|
||||
val result = service.restore(id)
|
||||
|
||||
assertEquals(id, result.id)
|
||||
assertEquals("Titel v2", result.title)
|
||||
assertEquals("Inhalt v2", result.content)
|
||||
assertEquals(4, result.version)
|
||||
assertEquals(ChangeType.RESTORED, historyEntry.captured.changeType)
|
||||
result.id shouldBe id
|
||||
result.title shouldBe "Titel v2"
|
||||
result.content shouldBe "Inhalt v2"
|
||||
result.version shouldBe 4
|
||||
historyEntry.captured.changeType shouldBe ChangeType.RESTORED
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -169,8 +169,8 @@ class DocumentServiceTest {
|
||||
|
||||
val result = service.restore(id, targetVersion = 1)
|
||||
|
||||
assertEquals("Titel v1", result.title)
|
||||
assertEquals(4, result.version)
|
||||
result.title shouldBe "Titel v1"
|
||||
result.version shouldBe 4
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -181,7 +181,7 @@ class DocumentServiceTest {
|
||||
)
|
||||
every { repository.findById(id) } returns sampleDocument(id = id)
|
||||
|
||||
assertThrows(DocumentConflictException::class.java) { service.restore(id) }
|
||||
shouldThrow<DocumentConflictException> { service.restore(id) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -189,7 +189,7 @@ class DocumentServiceTest {
|
||||
val id = UUID.randomUUID()
|
||||
every { historyRepository.findByDocumentId(id) } returns emptyList()
|
||||
|
||||
assertThrows(DocumentNotFoundException::class.java) { service.restore(id) }
|
||||
shouldThrow<DocumentNotFoundException> { service.restore(id) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -197,7 +197,7 @@ class DocumentServiceTest {
|
||||
val id = UUID.randomUUID()
|
||||
every { repository.findById(id) } returns null
|
||||
|
||||
assertThrows(DocumentNotFoundException::class.java) { service.findById(id) }
|
||||
shouldThrow<DocumentNotFoundException> { service.findById(id) }
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -205,6 +205,6 @@ class DocumentServiceTest {
|
||||
val docs = listOf(sampleDocument(), sampleDocument())
|
||||
every { repository.findAll() } returns docs
|
||||
|
||||
assertEquals(docs, service.findAll())
|
||||
service.findAll() shouldBe docs
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user