refactoring Single Source of Truth
This commit is contained in:
@@ -0,0 +1,508 @@
|
||||
# Architecture Principles und Grundsätze
|
||||
|
||||
---
|
||||
guideline_type: "project-standards"
|
||||
scope: "architecture-principles"
|
||||
audience: ["developers", "architects", "ai-assistants"]
|
||||
last_updated: "2025-09-13"
|
||||
dependencies: ["master-guideline.md"]
|
||||
related_files: ["build.gradle.kts", "settings.gradle.kts", "docker-compose.yml"]
|
||||
ai_context: "Architectural foundations, microservices patterns, DDD principles, event-driven architecture, and multiplatform strategy"
|
||||
---
|
||||
|
||||
## 🏗️ Vision & Architektonische Grundpfeiler
|
||||
|
||||
Dieses Dokument definiert die verbindlichen technischen Richtlinien und Qualitätsstandards für das Projekt "Meldestelle_Pro". Ziel ist die Schaffung einer modernen, skalierbaren und wartbaren Plattform für den Pferdesport.
|
||||
|
||||
> **🤖 AI-Assistant Hinweis:**
|
||||
> Die Architektur basiert auf vier Kernsäulen:
|
||||
> - **Microservices:** Modularität & Skalierbarkeit
|
||||
> - **DDD:** Fachlichkeit im Code
|
||||
> - **EDA:** Ereignisgesteuerte Entkopplung
|
||||
> - **KMP:** Kotlin Multiplatform für Effizienz
|
||||
|
||||
### Die vier Säulen der Architektur
|
||||
|
||||
1. **Modularität & Skalierbarkeit** durch eine **Microservices-Architektur**
|
||||
2. **Fachlichkeit im Code** durch **Domain-Driven Design (DDD)**
|
||||
3. **Entkopplung & Resilienz** durch eine **ereignisgesteuerte Architektur (EDA)**
|
||||
4. **Effizienz & Konsistenz** durch eine **Multiplattform-Client-Strategie (KMP)**
|
||||
|
||||
> **Grundsatz:** Jede Code-Änderung muss diese vier Grundprinzipien respektieren.
|
||||
|
||||
## 🎯 AI-Assistenten: Architektur-Schnellreferenz
|
||||
|
||||
### Architektur-Säulen im Detail
|
||||
|
||||
| Säule | Technologie | Zweck | Umsetzung |
|
||||
|-------|------------|-------|-----------|
|
||||
| Microservices | Spring Boot, Docker | Modularität & Skalierbarkeit | Service-per-Domain-Pattern |
|
||||
| DDD | Kotlin, Clean Architecture | Fachlichkeit im Code | Bounded Contexts, Domain Events |
|
||||
| EDA | Kafka, Events | Entkopplung & Resilienz | Asynchrone Kommunikation |
|
||||
| KMP | Kotlin Multiplatform | Effizienz & Konsistenz | Shared Business Logic |
|
||||
|
||||
## 🔧 Backend-Entwicklungsrichtlinien
|
||||
|
||||
### Microservice-Struktur (Clean Architecture)
|
||||
|
||||
Jeder fachliche Microservice folgt der 4-Layer-Struktur (`api`, `application`, `domain`, `infrastructure`).
|
||||
|
||||
```
|
||||
service-name/
|
||||
├── service-name-api/ # REST-Endpoints, DTOs
|
||||
├── service-name-application/ # Use Cases, Commands, Queries
|
||||
├── service-name-domain/ # Domain Models, Events, Services
|
||||
└── service-name-infrastructure/ # Repositories, External Services
|
||||
```
|
||||
|
||||
#### Layer-Verantwortlichkeiten
|
||||
|
||||
**API Layer (`-api`):**
|
||||
```kotlin
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/members")
|
||||
class MemberController(
|
||||
private val memberService: MemberService
|
||||
) {
|
||||
@PostMapping
|
||||
fun createMember(@RequestBody request: CreateMemberRequest): ResponseEntity<MemberResponse> {
|
||||
val command = CreateMemberCommand(
|
||||
name = request.name,
|
||||
email = request.email,
|
||||
licenseNumber = request.licenseNumber
|
||||
)
|
||||
|
||||
return when (val result = memberService.createMember(command)) {
|
||||
is Result.Success -> ResponseEntity.ok(result.value.toResponse())
|
||||
is Result.Failure -> ResponseEntity.badRequest().body(result.error.toErrorResponse())
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Application Layer (`-application`):**
|
||||
```kotlin
|
||||
@Service
|
||||
class MemberService(
|
||||
private val memberRepository: MemberRepository,
|
||||
private val eventPublisher: EventPublisher
|
||||
) {
|
||||
suspend fun createMember(command: CreateMemberCommand): Result<Member, BusinessError> {
|
||||
// Validation
|
||||
val validationResult = validateCreateMemberCommand(command)
|
||||
if (validationResult is Result.Failure) {
|
||||
return validationResult
|
||||
}
|
||||
|
||||
// Business Logic
|
||||
val member = Member.create(
|
||||
name = command.name,
|
||||
email = command.email,
|
||||
licenseNumber = command.licenseNumber
|
||||
)
|
||||
|
||||
// Persistence
|
||||
return memberRepository.save(member).map {
|
||||
// Event Publishing
|
||||
eventPublisher.publish(MemberCreatedEvent(member))
|
||||
member
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Domain Layer (`-domain`):**
|
||||
```kotlin
|
||||
@JvmInline
|
||||
value class MemberId(val value: UUID) {
|
||||
companion object {
|
||||
fun generate(): MemberId = MemberId(UUID.randomUUID())
|
||||
}
|
||||
}
|
||||
|
||||
data class Member private constructor(
|
||||
val id: MemberId,
|
||||
val name: String,
|
||||
val email: Email,
|
||||
val licenseNumber: LicenseNumber,
|
||||
val status: MemberStatus = MemberStatus.PENDING
|
||||
) {
|
||||
companion object {
|
||||
fun create(
|
||||
name: String,
|
||||
email: String,
|
||||
licenseNumber: String
|
||||
): Result<Member, ValidationError> {
|
||||
return Result.Success(
|
||||
Member(
|
||||
id = MemberId.generate(),
|
||||
name = name,
|
||||
email = Email.of(email).getOrThrow(),
|
||||
licenseNumber = LicenseNumber.of(licenseNumber).getOrThrow()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun activate(): Member = copy(status = MemberStatus.ACTIVE)
|
||||
fun suspend(): Member = copy(status = MemberStatus.SUSPENDED)
|
||||
}
|
||||
```
|
||||
|
||||
**Infrastructure Layer (`-infrastructure`):**
|
||||
```kotlin
|
||||
@Repository
|
||||
class PostgresMemberRepository(
|
||||
private val jdbcTemplate: JdbcTemplate
|
||||
) : MemberRepository {
|
||||
|
||||
override suspend fun save(member: Member): Result<Unit, RepositoryError> {
|
||||
return try {
|
||||
jdbcTemplate.update(
|
||||
"INSERT INTO members (id, name, email, license_number, status) VALUES (?, ?, ?, ?, ?)",
|
||||
member.id.value,
|
||||
member.name,
|
||||
member.email.value,
|
||||
member.licenseNumber.value,
|
||||
member.status.name
|
||||
)
|
||||
Result.Success(Unit)
|
||||
} catch (e: DataAccessException) {
|
||||
Result.Failure(RepositoryError.DATABASE_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun findById(id: MemberId): Result<Member?, RepositoryError> {
|
||||
return try {
|
||||
val member = jdbcTemplate.queryForObject(
|
||||
"SELECT * FROM members WHERE id = ?",
|
||||
arrayOf(id.value)
|
||||
) { rs, _ ->
|
||||
Member(
|
||||
id = MemberId(UUID.fromString(rs.getString("id"))),
|
||||
name = rs.getString("name"),
|
||||
email = Email.of(rs.getString("email")).getOrThrow(),
|
||||
licenseNumber = LicenseNumber.of(rs.getString("license_number")).getOrThrow(),
|
||||
status = MemberStatus.valueOf(rs.getString("status"))
|
||||
)
|
||||
}
|
||||
Result.Success(member)
|
||||
} catch (e: EmptyResultDataAccessException) {
|
||||
Result.Success(null)
|
||||
} catch (e: DataAccessException) {
|
||||
Result.Failure(RepositoryError.DATABASE_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Repository-Pattern
|
||||
|
||||
Jede Repository-Methode muss das `Result`-Pattern verwenden.
|
||||
|
||||
```kotlin
|
||||
interface MemberRepository {
|
||||
suspend fun findById(id: MemberId): Result<Member?, RepositoryError>
|
||||
suspend fun save(member: Member): Result<Unit, RepositoryError>
|
||||
suspend fun findByEmail(email: Email): Result<Member?, RepositoryError>
|
||||
suspend fun findByLicenseNumber(licenseNumber: LicenseNumber): Result<Member?, RepositoryError>
|
||||
suspend fun findAll(pageable: Pageable): Result<Page<Member>, RepositoryError>
|
||||
}
|
||||
```
|
||||
|
||||
### Messaging & Event-Naming
|
||||
|
||||
Event-Naming Convention: Domänen-Events folgen dem Muster `{Domain}{Entity}{Action}Event`.
|
||||
|
||||
```kotlin
|
||||
data class MemberPersonalDataUpdatedEvent(
|
||||
val memberId: MemberId,
|
||||
val oldName: String,
|
||||
val newName: String,
|
||||
val oldEmail: Email,
|
||||
val newEmail: Email,
|
||||
val updatedAt: Instant = Instant.now(),
|
||||
val correlationId: String = MDC.get("correlationId") ?: UUID.randomUUID().toString()
|
||||
) : DomainEvent {
|
||||
override val eventType: String = "member.personal-data.updated"
|
||||
override val aggregateId: String = memberId.value.toString()
|
||||
override val version: Int = 1
|
||||
}
|
||||
```
|
||||
|
||||
## 📱 Frontend-Entwicklungsrichtlinien
|
||||
|
||||
Das Frontend folgt konsequent dem **Model-View-ViewModel (MVVM)**-Muster und der **Kotlin Multiplatform (KMP)**-Strategie. Der UI-Code wird nach **fachlichen Features** (vertikale Schnitte) strukturiert.
|
||||
|
||||
### Multiplatform-Struktur
|
||||
|
||||
```
|
||||
client/
|
||||
├── src/commonMain/kotlin/ # Shared Business Logic
|
||||
│ ├── domain/ # Domain Models
|
||||
│ ├── data/ # Repositories, API-Clients
|
||||
│ ├── presentation/ # ViewModels, UI-States
|
||||
│ └── ui/ # Shared UI-Components
|
||||
├── src/jvmMain/kotlin/ # Desktop-spezifischer Code
|
||||
│ └── ui/ # Desktop UI-Adaptierungen
|
||||
└── src/wasmJsMain/kotlin/ # Web-spezifischer Code
|
||||
└── ui/ # Web UI-Adaptierungen
|
||||
```
|
||||
|
||||
### MVVM-Implementation
|
||||
|
||||
**Shared ViewModel (commonMain):**
|
||||
```kotlin
|
||||
class MemberListViewModel(
|
||||
private val memberRepository: MemberRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(MemberListUiState())
|
||||
val uiState: StateFlow<MemberListUiState> = _uiState.asStateFlow()
|
||||
|
||||
fun loadMembers() {
|
||||
viewModelScope.launch {
|
||||
_uiState.value = _uiState.value.copy(isLoading = true)
|
||||
|
||||
when (val result = memberRepository.getAllMembers()) {
|
||||
is Result.Success -> {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
members = result.value,
|
||||
error = null
|
||||
)
|
||||
}
|
||||
is Result.Failure -> {
|
||||
_uiState.value = _uiState.value.copy(
|
||||
isLoading = false,
|
||||
error = result.error.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class MemberListUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val members: List<Member> = emptyList(),
|
||||
val error: String? = null
|
||||
)
|
||||
```
|
||||
|
||||
**Shared UI-Component (commonMain):**
|
||||
```kotlin
|
||||
@Composable
|
||||
fun MemberListScreen(
|
||||
viewModel: MemberListViewModel = viewModel()
|
||||
) {
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
viewModel.loadMembers()
|
||||
}
|
||||
|
||||
Column {
|
||||
if (uiState.isLoading) {
|
||||
CircularProgressIndicator()
|
||||
}
|
||||
|
||||
uiState.error?.let { error ->
|
||||
Text(
|
||||
text = error,
|
||||
color = MaterialTheme.colorScheme.error
|
||||
)
|
||||
}
|
||||
|
||||
LazyColumn {
|
||||
items(uiState.members) { member ->
|
||||
MemberCard(
|
||||
member = member,
|
||||
onMemberClick = { /* Handle click */ }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 Domain-Driven Design (DDD) Patterns
|
||||
|
||||
### Bounded Contexts
|
||||
|
||||
```
|
||||
Meldestelle-Domain/
|
||||
├── member-context/ # Mitgliederverwaltung
|
||||
├── tournament-context/ # Turnierverwaltung
|
||||
├── horse-context/ # Pferdeverwaltung
|
||||
├── registration-context/ # Anmeldungen
|
||||
└── payment-context/ # Zahlungsabwicklung
|
||||
```
|
||||
|
||||
### Aggregate Design
|
||||
|
||||
```kotlin
|
||||
class Tournament private constructor(
|
||||
val id: TournamentId,
|
||||
val name: String,
|
||||
val startDate: LocalDate,
|
||||
val endDate: LocalDate,
|
||||
val maxParticipants: Int,
|
||||
private val registrations: MutableList<TournamentRegistration> = mutableListOf()
|
||||
) {
|
||||
companion object {
|
||||
fun create(
|
||||
name: String,
|
||||
startDate: LocalDate,
|
||||
endDate: LocalDate,
|
||||
maxParticipants: Int
|
||||
): Result<Tournament, ValidationError> {
|
||||
// Business rules validation
|
||||
if (startDate.isAfter(endDate)) {
|
||||
return Result.Failure(ValidationError.INVALID_DATE_RANGE)
|
||||
}
|
||||
|
||||
return Result.Success(
|
||||
Tournament(
|
||||
id = TournamentId.generate(),
|
||||
name = name,
|
||||
startDate = startDate,
|
||||
endDate = endDate,
|
||||
maxParticipants = maxParticipants
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun registerMember(memberId: MemberId): Result<TournamentRegistrationCreatedEvent, BusinessError> {
|
||||
// Business rules
|
||||
if (registrations.size >= maxParticipants) {
|
||||
return Result.Failure(BusinessError.TOURNAMENT_FULL)
|
||||
}
|
||||
|
||||
if (registrations.any { it.memberId == memberId }) {
|
||||
return Result.Failure(BusinessError.ALREADY_REGISTERED)
|
||||
}
|
||||
|
||||
val registration = TournamentRegistration(
|
||||
id = TournamentRegistrationId.generate(),
|
||||
tournamentId = id,
|
||||
memberId = memberId,
|
||||
registrationDate = LocalDateTime.now()
|
||||
)
|
||||
|
||||
registrations.add(registration)
|
||||
|
||||
return Result.Success(
|
||||
TournamentRegistrationCreatedEvent(
|
||||
tournamentId = id,
|
||||
memberId = memberId,
|
||||
registrationId = registration.id
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Infrastructure & Betrieb
|
||||
|
||||
#### Kafka-Konfiguration
|
||||
|
||||
Die Konfiguration muss auf maximale Zuverlässigkeit ausgelegt sein:
|
||||
|
||||
```yaml
|
||||
# application.yml
|
||||
kafka:
|
||||
producer:
|
||||
acks: all
|
||||
enable-idempotence: true
|
||||
max-in-flight-requests-per-connection: 1
|
||||
consumer:
|
||||
group-id-prefix: "meldestelle-${spring.application.name}"
|
||||
auto-offset-reset: earliest
|
||||
enable-auto-commit: false
|
||||
```
|
||||
|
||||
#### Datenbank-Migrationen (Flyway)
|
||||
|
||||
Migrations-Skripte müssen einer klaren Namenskonvention folgen:
|
||||
|
||||
* **Pattern:** `V{version}__{description}.sql` (z.B., `V001__Create_member_tables.sql`)
|
||||
* **Repeatable:** `R__{description}.sql` (z.B., `R__Update_member_view.sql`)
|
||||
|
||||
#### API-Dokumentation (OpenAPI)
|
||||
|
||||
Alle öffentlichen REST-Endpunkte müssen mit OpenAPI-Annotationen (`@Operation`, `@ApiResponse`) dokumentiert werden, um eine klare und interaktive API-Dokumentation zu generieren.
|
||||
|
||||
```kotlin
|
||||
@Operation(
|
||||
summary = "Neues Mitglied erstellen",
|
||||
description = "Erstellt ein neues Mitglied mit den angegebenen Daten"
|
||||
)
|
||||
@ApiResponses(
|
||||
value = [
|
||||
ApiResponse(
|
||||
responseCode = "201",
|
||||
description = "Mitglied erfolgreich erstellt"
|
||||
),
|
||||
ApiResponse(
|
||||
responseCode = "400",
|
||||
description = "Ungültige Eingabedaten"
|
||||
)
|
||||
]
|
||||
)
|
||||
@PostMapping
|
||||
fun createMember(@RequestBody request: CreateMemberRequest): ResponseEntity<MemberResponse>
|
||||
```
|
||||
|
||||
## 🚀 Architektur-Entscheidungen (ADRs)
|
||||
|
||||
### ADR-001: Microservices mit Domain-Driven Design
|
||||
|
||||
**Status:** Akzeptiert
|
||||
|
||||
**Kontext:** Skalierbare und wartbare Architektur für Pferdesport-Plattform
|
||||
|
||||
**Entscheidung:** Microservices-Architektur mit DDD-Bounded-Contexts
|
||||
|
||||
**Konsequenzen:**
|
||||
- ✅ Unabhängige Entwicklung und Deployment
|
||||
- ✅ Fachliche Kapselung durch Bounded Contexts
|
||||
- ❌ Komplexität bei Service-zu-Service-Kommunikation
|
||||
- ❌ Eventual Consistency zwischen Services
|
||||
|
||||
### ADR-002: Event-Driven Architecture mit Kafka
|
||||
|
||||
**Status:** Akzeptiert
|
||||
|
||||
**Kontext:** Entkopplung und Resilienz zwischen Services
|
||||
|
||||
**Entscheidung:** Kafka als zentraler Event-Broker
|
||||
|
||||
**Konsequenzen:**
|
||||
- ✅ Lose Kopplung zwischen Services
|
||||
- ✅ Audit-Log durch Event-Store
|
||||
- ❌ Komplexität bei Event-Schema-Evolution
|
||||
- ❌ Eventually Consistent State
|
||||
|
||||
### ADR-003: Kotlin Multiplatform für Client
|
||||
|
||||
**Status:** Akzeptiert
|
||||
|
||||
**Kontext:** Code-Sharing zwischen Desktop und Web
|
||||
|
||||
**Entscheidung:** KMP mit Compose Multiplatform
|
||||
|
||||
**Konsequenzen:**
|
||||
- ✅ Geteilte Business-Logic
|
||||
- ✅ Einheitliche UI-Patterns
|
||||
- ❌ Plattform-spezifische Optimierungen schwieriger
|
||||
- ❌ Abhängigkeit von Kotlin/JetBrains-Ökosystem
|
||||
|
||||
---
|
||||
|
||||
**Navigation:**
|
||||
- [Master-Guideline](../master-guideline.md) - Übergeordnete Projektrichtlinien
|
||||
- [Coding-Standards](./coding-standards.md) - Code-Qualitätsstandards
|
||||
- [Testing-Standards](./testing-standards.md) - Test-Qualitätssicherung
|
||||
- [Documentation-Standards](./documentation-standards.md) - Dokumentationsrichtlinien
|
||||
@@ -0,0 +1,216 @@
|
||||
# Coding Standards und Code-Qualität
|
||||
|
||||
---
|
||||
guideline_type: "project-standards"
|
||||
scope: "coding-standards"
|
||||
audience: ["developers", "ai-assistants"]
|
||||
last_updated: "2025-09-13"
|
||||
dependencies: ["master-guideline.md"]
|
||||
related_files: ["build.gradle.kts", "detekt.yml", "*.kt"]
|
||||
ai_context: "Coding conventions, naming standards, type safety, error handling, and logging practices"
|
||||
---
|
||||
|
||||
## 📋 Coding Conventions & Code-Qualität
|
||||
|
||||
### Sprach- und Stilstandards
|
||||
|
||||
* **Primärsprache:** Kotlin (JVM/Multiplatform)
|
||||
* **Java-Kompatibilität:** Ziel ist Java 21+
|
||||
* **Code-Stil:** Offizielle Kotlin Coding Conventions, durch `Detekt` geprüft.
|
||||
|
||||
> **🤖 AI-Assistant Hinweis:**
|
||||
> Alle Kotlin-Code muss den offiziellen Kotlin Coding Conventions entsprechen:
|
||||
> - **Detekt-Validierung:** Automatische Code-Style-Prüfung
|
||||
> - **Java 21+ Kompatibilität:** Nutze moderne Java-Features wo sinnvoll
|
||||
> - **Multiplatform:** Code sollte plattformübergreifend funktionieren
|
||||
|
||||
### Namenskonventionen
|
||||
|
||||
* **Klassen & Interfaces:** `PascalCase` (z.B. `MemberService`, `EventRepository`)
|
||||
* **Funktionen & Variablen:** `camelCase` (z.B. `authenticateUser`, `memberRepository`)
|
||||
* **Testmethoden:** Beschreibend mit Backticks (z.B. `` `should return Success for valid credentials` ``)
|
||||
* **Konstanten:** `SCREAMING_SNAKE_CASE` (z.B. `MAX_RETRY_ATTEMPTS`)
|
||||
* **Enums:** `PascalCase` für Werte (z.B. `MemberStatus.ACTIVE`)
|
||||
|
||||
### Value Classes für Typsicherheit
|
||||
|
||||
Primitive Typen (UUID, String, Long) für IDs oder spezifische Werte müssen in typsichere `value class`-Wrapper gekapselt werden.
|
||||
|
||||
```kotlin
|
||||
@JvmInline
|
||||
value class MemberId(val value: UUID) {
|
||||
companion object {
|
||||
fun of(value: String): Result<MemberId, ValidationError> =
|
||||
runCatching { UUID.fromString(value) }
|
||||
.map { MemberId(it) }
|
||||
.mapError { ValidationError.INVALID_UUID }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error-Handling & Logging
|
||||
|
||||
* **`Result`-Pattern:** Für erwartbare Geschäftsfehler ist das `Result`-Pattern zu verwenden. Exceptions sind für unerwartete, technische Fehler reserviert.
|
||||
|
||||
* **Fehler-Hierarchie:** Wir verwenden eine `sealed class`-Hierarchie, um Fehlerarten klar zu kategorisieren (`DomainError`, `ValidationError`, `BusinessError`, `TechnicalError`).
|
||||
|
||||
* **Structured Logging:** Logs müssen strukturiert sein und eine Korrelations-ID enthalten, um Anfragen über Service-Grenzen hinweg zu verfolgen.
|
||||
|
||||
```kotlin
|
||||
logger.info {
|
||||
"Creating member" with mapOf(
|
||||
"memberId" to command.memberId.value,
|
||||
"correlationId" to MDC.get("correlationId")
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 AI-Assistenten: Coding-Standards-Schnellreferenz
|
||||
|
||||
### Namenskonventionen-Übersicht
|
||||
|
||||
| Element | Convention | Beispiel |
|
||||
|---------|------------|----------|
|
||||
| Klassen/Interfaces | PascalCase | `MemberService`, `EventRepository` |
|
||||
| Funktionen/Variablen | camelCase | `authenticateUser`, `memberRepository` |
|
||||
| Konstanten | SCREAMING_SNAKE_CASE | `MAX_RETRY_ATTEMPTS` |
|
||||
| Test-Methoden | Backticks beschreibend | `` `should return Success for valid credentials` `` |
|
||||
| Enum-Werte | PascalCase | `MemberStatus.ACTIVE` |
|
||||
|
||||
### Code-Qualitäts-Checkliste
|
||||
|
||||
- [ ] **Detekt-Prüfung:** Code-Stil entspricht Kotlin Conventions
|
||||
- [ ] **Value Classes:** Primitive Typen sind in typsichere Wrapper gekapselt
|
||||
- [ ] **Result-Pattern:** Geschäftsfehler verwenden Result statt Exceptions
|
||||
- [ ] **Structured Logging:** Logs enthalten Korrelations-IDs
|
||||
- [ ] **Error-Hierarchie:** Sealed Classes für Fehlerkategorisierung
|
||||
|
||||
### Häufige Code-Patterns
|
||||
|
||||
#### Typsichere IDs
|
||||
```kotlin
|
||||
@JvmInline
|
||||
value class EntityId(val value: UUID) {
|
||||
companion object {
|
||||
fun generate(): EntityId = EntityId(UUID.randomUUID())
|
||||
fun of(value: String): Result<EntityId, ValidationError> =
|
||||
runCatching { UUID.fromString(value) }
|
||||
.map { EntityId(it) }
|
||||
.mapError { ValidationError.INVALID_UUID }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error-Handling mit Result
|
||||
```kotlin
|
||||
interface EntityRepository {
|
||||
suspend fun findById(id: EntityId): Result<Entity?, RepositoryError>
|
||||
suspend fun save(entity: Entity): Result<Unit, RepositoryError>
|
||||
}
|
||||
|
||||
// Verwendung
|
||||
when (val result = repository.findById(entityId)) {
|
||||
is Result.Success -> processEntity(result.value)
|
||||
is Result.Failure -> handleError(result.error)
|
||||
}
|
||||
```
|
||||
|
||||
#### Structured Logging
|
||||
```kotlin
|
||||
class EntityService {
|
||||
private val logger = LoggerFactory.getLogger(EntityService::class.java)
|
||||
|
||||
suspend fun processEntity(command: ProcessEntityCommand): Result<Unit, ProcessingError> {
|
||||
val correlationId = MDC.get("correlationId")
|
||||
|
||||
logger.info {
|
||||
"Processing entity" with mapOf(
|
||||
"entityId" to command.entityId.value,
|
||||
"correlationId" to correlationId,
|
||||
"operation" to "process"
|
||||
)
|
||||
}
|
||||
|
||||
return try {
|
||||
// Processing logic
|
||||
Result.Success(Unit)
|
||||
} catch (e: Exception) {
|
||||
logger.error {
|
||||
"Entity processing failed" with mapOf(
|
||||
"entityId" to command.entityId.value,
|
||||
"correlationId" to correlationId,
|
||||
"error" to e.message
|
||||
)
|
||||
}
|
||||
Result.Failure(ProcessingError.TECHNICAL_ERROR)
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Sealed Class Hierarchie für Fehler
|
||||
```kotlin
|
||||
sealed interface DomainError {
|
||||
val message: String
|
||||
val code: String
|
||||
}
|
||||
|
||||
sealed interface ValidationError : DomainError {
|
||||
data object INVALID_UUID : ValidationError {
|
||||
override val message = "Invalid UUID format"
|
||||
override val code = "VALIDATION_INVALID_UUID"
|
||||
}
|
||||
|
||||
data object REQUIRED_FIELD_MISSING : ValidationError {
|
||||
override val message = "Required field is missing"
|
||||
override val code = "VALIDATION_REQUIRED_FIELD_MISSING"
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface BusinessError : DomainError {
|
||||
data object ENTITY_NOT_FOUND : BusinessError {
|
||||
override val message = "Entity not found"
|
||||
override val code = "BUSINESS_ENTITY_NOT_FOUND"
|
||||
}
|
||||
}
|
||||
|
||||
sealed interface TechnicalError : DomainError {
|
||||
data object DATABASE_CONNECTION_FAILED : TechnicalError {
|
||||
override val message = "Database connection failed"
|
||||
override val code = "TECHNICAL_DATABASE_CONNECTION_FAILED"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Detekt-Konfiguration
|
||||
|
||||
Wichtige Detekt-Regeln für das Projekt:
|
||||
|
||||
```yaml
|
||||
# detekt.yml
|
||||
style:
|
||||
MaxLineLength:
|
||||
maxLineLength: 120
|
||||
FunctionNaming:
|
||||
functionPattern: '^[a-z][a-zA-Z0-9]*$'
|
||||
ClassNaming:
|
||||
classPattern: '^[A-Z][a-zA-Z0-9]*$'
|
||||
|
||||
complexity:
|
||||
ComplexMethod:
|
||||
threshold: 15
|
||||
LongParameterList:
|
||||
functionThreshold: 6
|
||||
|
||||
potential-bugs:
|
||||
UnsafeCallOnNullableType:
|
||||
active: true
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Navigation:**
|
||||
- [Master-Guideline](../master-guideline.md) - Übergeordnete Projektrichtlinien
|
||||
- [Testing-Standards](./testing-standards.md) - Test-Qualitätsstandards
|
||||
- [Documentation-Standards](./documentation-standards.md) - Dokumentationsrichtlinien
|
||||
- [Architecture-Principles](./architecture-principles.md) - Architektur-Grundsätze
|
||||
@@ -0,0 +1,326 @@
|
||||
# Documentation Standards
|
||||
|
||||
---
|
||||
guideline_type: "project-standards"
|
||||
scope: "documentation-standards"
|
||||
audience: ["developers", "ai-assistants", "technical-writers"]
|
||||
last_updated: "2025-09-13"
|
||||
dependencies: ["master-guideline.md"]
|
||||
related_files: ["README*.md", "docs/**", "*.md", "openapi.yaml"]
|
||||
ai_context: "Documentation language standards, README structure, API documentation, and technical writing guidelines"
|
||||
---
|
||||
|
||||
## 📝 Dokumentationsstandards
|
||||
|
||||
### Sprache für Dokumentation
|
||||
|
||||
* **README-Dateien:** Alle README-Dokumentationen im Projekt müssen in **deutscher Sprache** verfasst werden. Dies gewährleistet Konsistenz und Zugänglichkeit für das deutsche Entwicklungsteam.
|
||||
|
||||
* **Code-Kommentare:** Komplexe Geschäftslogik und fachliche Zusammenhänge sollen in deutscher Sprache kommentiert werden.
|
||||
|
||||
* **API-Dokumentation:** OpenAPI-Beschreibungen und -Beispiele sind bevorzugt in deutscher Sprache zu verfassen, sofern keine internationalen Anforderungen bestehen.
|
||||
|
||||
> **🤖 AI-Assistant Hinweis:**
|
||||
> Dokumentationssprache-Regeln:
|
||||
> - **README-Dateien:** Immer Deutsch
|
||||
> - **Code-Kommentare:** Deutsch für Geschäftslogik, Englisch für technische Details
|
||||
> - **API-Docs:** Deutsch bevorzugt, Englisch bei internationalen APIs
|
||||
> - **Technische Begriffe:** Englische Originalform wenn keine deutsche Übersetzung etabliert
|
||||
|
||||
### Dokumentationsstruktur
|
||||
|
||||
* README-Dateien sollen eine einheitliche Struktur befolgen: Überblick, Architektur, Entwicklung, Tests, Deployment.
|
||||
|
||||
* Technische Begriffe dürfen in englischer Originalform verwendet werden, wenn keine etablierte deutsche Übersetzung existiert.
|
||||
|
||||
## 🎯 AI-Assistenten: Documentation-Schnellreferenz
|
||||
|
||||
### README-Template-Struktur
|
||||
|
||||
```markdown
|
||||
# [Projekt/Modul Name]
|
||||
|
||||
## Überblick
|
||||
[Kurze Beschreibung des Zwecks und der Funktionalität]
|
||||
|
||||
## Architektur
|
||||
[Architektonische Entscheidungen und Komponenten-Übersicht]
|
||||
|
||||
## Entwicklung
|
||||
[Setup-Anweisungen für lokale Entwicklung]
|
||||
|
||||
### Voraussetzungen
|
||||
[Erforderliche Tools und Versionen]
|
||||
|
||||
### Installation
|
||||
[Schritt-für-Schritt Setup-Anleitung]
|
||||
|
||||
### Konfiguration
|
||||
[Wichtige Konfigurationsoptionen]
|
||||
|
||||
## Tests
|
||||
[Test-Ausführung und Test-Strategie]
|
||||
|
||||
## Deployment
|
||||
[Deployment-Anweisungen für verschiedene Umgebungen]
|
||||
|
||||
## API-Dokumentation
|
||||
[Links zu API-Docs oder eingebettete Dokumentation]
|
||||
|
||||
## Troubleshooting
|
||||
[Häufige Probleme und Lösungen]
|
||||
```
|
||||
|
||||
### Code-Kommentar-Standards
|
||||
|
||||
#### Deutsche Geschäftslogik-Kommentare
|
||||
```kotlin
|
||||
/**
|
||||
* Prüft, ob ein Mitglied für die Anmeldung zu einem Turnier berechtigt ist.
|
||||
*
|
||||
* Ein Mitglied ist berechtigt, wenn:
|
||||
* - Der Mitgliedsstatus AKTIV ist
|
||||
* - Die Lizenz gültig und nicht suspendiert ist
|
||||
* - Keine offenen Zahlungen vorliegen
|
||||
*/
|
||||
fun isEligibleForTournament(member: Member, tournament: Tournament): Result<Boolean, ValidationError> {
|
||||
// Mitgliedsstatus prüfen
|
||||
if (member.status != MemberStatus.ACTIVE) {
|
||||
return Result.Failure(ValidationError.MEMBER_NOT_ACTIVE)
|
||||
}
|
||||
|
||||
// Lizenzvalidierung durchführen
|
||||
return validateLicense(member, tournament)
|
||||
}
|
||||
```
|
||||
|
||||
#### Englische technische Kommentare
|
||||
```kotlin
|
||||
/**
|
||||
* Cache implementation using Redis with TTL support
|
||||
* Performance: O(1) for get/set operations
|
||||
*/
|
||||
class RedisCache<T>(
|
||||
private val redisClient: RedisClient,
|
||||
private val ttl: Duration = Duration.ofHours(1)
|
||||
) : Cache<T> {
|
||||
|
||||
override suspend fun get(key: String): T? {
|
||||
// Use Redis GET command with automatic deserialization
|
||||
return redisClient.get(key)?.let {
|
||||
jsonMapper.readValue(it, typeRef<T>())
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### OpenAPI-Dokumentation Standards
|
||||
|
||||
#### Deutsche API-Beschreibungen
|
||||
```yaml
|
||||
openapi: 3.0.0
|
||||
info:
|
||||
title: Meldestelle API
|
||||
description: REST API für die Verwaltung von Pferdesport-Meldungen
|
||||
version: 1.0.0
|
||||
|
||||
paths:
|
||||
/members:
|
||||
post:
|
||||
summary: Neues Mitglied anlegen
|
||||
description: |
|
||||
Erstellt ein neues Mitglied in der Datenbank.
|
||||
Validiert alle Pflichtfelder und prüft auf Duplikate.
|
||||
requestBody:
|
||||
description: Mitgliedsdaten für die Erstellung
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/CreateMemberRequest'
|
||||
example:
|
||||
name: "Max Mustermann"
|
||||
email: "max.mustermann@example.com"
|
||||
licenseNumber: "12345"
|
||||
responses:
|
||||
'201':
|
||||
description: Mitglied erfolgreich erstellt
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Member'
|
||||
'400':
|
||||
description: Ungültige Eingabedaten
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ValidationError'
|
||||
|
||||
components:
|
||||
schemas:
|
||||
Member:
|
||||
type: object
|
||||
description: Repräsentiert ein Mitglied im System
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Eindeutige Mitglieds-ID
|
||||
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||
name:
|
||||
type: string
|
||||
description: Vollständiger Name des Mitglieds
|
||||
example: "Max Mustermann"
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: E-Mail-Adresse des Mitglieds
|
||||
example: "max.mustermann@example.com"
|
||||
```
|
||||
|
||||
### Dokumentations-Checkliste
|
||||
|
||||
#### README-Dateien
|
||||
- [ ] **Struktur:** Folgt dem Standard-Template
|
||||
- [ ] **Sprache:** Auf Deutsch verfasst
|
||||
- [ ] **Aktualität:** Entspricht dem aktuellen Code-Stand
|
||||
- [ ] **Vollständigkeit:** Alle erforderlichen Abschnitte vorhanden
|
||||
- [ ] **Beispiele:** Konkrete Code-Beispiele und Kommandos
|
||||
- [ ] **Links:** Funktionierende Verweise auf verwandte Dokumentation
|
||||
|
||||
#### API-Dokumentation
|
||||
- [ ] **OpenAPI-Spezifikation:** Vollständig und valide
|
||||
- [ ] **Deutsche Beschreibungen:** Für alle Endpunkte und Schemas
|
||||
- [ ] **Beispiele:** Realistische Request/Response-Beispiele
|
||||
- [ ] **Error-Handling:** Dokumentierte Fehlerfälle
|
||||
- [ ] **Authentifizierung:** Sicherheitsanforderungen dokumentiert
|
||||
|
||||
#### Code-Kommentare
|
||||
- [ ] **Geschäftslogik:** Deutsche Kommentare für fachliche Aspekte
|
||||
- [ ] **Technische Details:** Englische Kommentare für Framework-/Library-Code
|
||||
- [ ] **Komplexität:** Komplexe Algorithmen sind erklärt
|
||||
- [ ] **TODOs:** Mit Ticket-Referenzen versehen
|
||||
- [ ] **Javadoc/KDoc:** Für öffentliche APIs vollständig
|
||||
|
||||
### Dokumentations-Patterns
|
||||
|
||||
#### Architektur-Diagramme
|
||||
```markdown
|
||||
## System-Architektur
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Client Layer"
|
||||
WEB[Web App]
|
||||
MOBILE[Mobile App]
|
||||
end
|
||||
|
||||
subgraph "API Gateway"
|
||||
GW[API Gateway]
|
||||
end
|
||||
|
||||
subgraph "Service Layer"
|
||||
MS[Member Service]
|
||||
TS[Tournament Service]
|
||||
NS[Notification Service]
|
||||
end
|
||||
|
||||
subgraph "Data Layer"
|
||||
PG[(PostgreSQL)]
|
||||
RD[(Redis)]
|
||||
end
|
||||
|
||||
WEB --> GW
|
||||
MOBILE --> GW
|
||||
GW --> MS
|
||||
GW --> TS
|
||||
GW --> NS
|
||||
MS --> PG
|
||||
TS --> PG
|
||||
NS --> RD
|
||||
```
|
||||
```
|
||||
|
||||
#### Feature-Dokumentation
|
||||
```markdown
|
||||
## Feature: Turnier-Anmeldung
|
||||
|
||||
### Fachlicher Überblick
|
||||
Die Turnier-Anmeldung ermöglicht es Mitgliedern, sich für Turniere zu registrieren.
|
||||
|
||||
### User Stories
|
||||
- Als Mitglied möchte ich mich für ein Turnier anmelden können
|
||||
- Als Turnierleiter möchte ich Anmeldungen verwalten können
|
||||
|
||||
### Technische Umsetzung
|
||||
|
||||
#### API-Endpunkte
|
||||
- `POST /tournaments/{id}/registrations` - Anmeldung erstellen
|
||||
- `GET /tournaments/{id}/registrations` - Anmeldungen abrufen
|
||||
- `DELETE /registrations/{id}` - Anmeldung stornieren
|
||||
|
||||
#### Domain-Events
|
||||
- `TournamentRegistrationCreated` - Bei erfolgreicher Anmeldung
|
||||
- `TournamentRegistrationCancelled` - Bei Stornierung
|
||||
|
||||
### Business Rules
|
||||
1. Anmeldung nur für aktive Mitglieder möglich
|
||||
2. Anmeldeschluss muss beachtet werden
|
||||
3. Maximale Teilnehmerzahl darf nicht überschritten werden
|
||||
```
|
||||
|
||||
#### Troubleshooting-Dokumentation
|
||||
```markdown
|
||||
## Häufige Probleme
|
||||
|
||||
### Problem: Service startet nicht
|
||||
**Symptome:** Container bleibt im Status "Restarting"
|
||||
|
||||
**Ursachen:**
|
||||
- Datenbankverbindung fehlgeschlagen
|
||||
- Fehlende Environment-Variablen
|
||||
- Port bereits belegt
|
||||
|
||||
**Lösung:**
|
||||
1. Logs prüfen: `docker-compose logs service-name`
|
||||
2. Environment-Variablen validieren
|
||||
3. Port-Konflikte lösen: `netstat -tulpn | grep :8080`
|
||||
|
||||
### Problem: Langsame API-Antworten
|
||||
**Symptome:** Response-Zeiten > 2 Sekunden
|
||||
|
||||
**Debugging:**
|
||||
```bash
|
||||
# Database-Performance prüfen
|
||||
docker-compose exec postgres psql -c "SELECT * FROM pg_stat_activity;"
|
||||
|
||||
# Redis-Performance prüfen
|
||||
docker-compose exec redis redis-cli info stats
|
||||
```
|
||||
|
||||
**Optimierung:**
|
||||
- Database-Indizes überprüfen
|
||||
- Query-Performance analysieren
|
||||
- Cache-Hit-Rate optimieren
|
||||
```
|
||||
|
||||
### Versionierung und Updates
|
||||
|
||||
#### Dokumentations-Versionierung
|
||||
- README-Dateien werden mit dem Code versioniert
|
||||
- API-Dokumentation folgt Semantic Versioning
|
||||
- Changelog wird für breaking changes geführt
|
||||
|
||||
#### Update-Prozess
|
||||
1. **Code-Änderungen** → README aktualisieren
|
||||
2. **API-Änderungen** → OpenAPI-Spec anpassen
|
||||
3. **Architektur-Änderungen** → Diagramme überarbeiten
|
||||
4. **Deployment-Änderungen** → Deployment-Docs aktualisieren
|
||||
|
||||
---
|
||||
|
||||
**Navigation:**
|
||||
- [Master-Guideline](../master-guideline.md) - Übergeordnete Projektrichtlinien
|
||||
- [Coding-Standards](./coding-standards.md) - Code-Qualitätsstandards
|
||||
- [Testing-Standards](./testing-standards.md) - Test-Qualitätssicherung
|
||||
- [Architecture-Principles](./architecture-principles.md) - Architektur-Grundsätze
|
||||
@@ -0,0 +1,379 @@
|
||||
# Testing Standards und Qualitätssicherung
|
||||
|
||||
---
|
||||
guideline_type: "project-standards"
|
||||
scope: "testing-standards"
|
||||
audience: ["developers", "ai-assistants"]
|
||||
last_updated: "2025-09-13"
|
||||
dependencies: ["master-guideline.md", "coding-standards.md"]
|
||||
related_files: ["build.gradle.kts", "src/test/**", "testcontainers.properties"]
|
||||
ai_context: "Testing strategies, test pyramid, tools, coverage requirements, and debugging practices"
|
||||
---
|
||||
|
||||
## 🧪 Testing Standards
|
||||
|
||||
Tests sind ein integraler Bestandteil jedes Features und müssen einen hohen Standard erfüllen.
|
||||
|
||||
> **🤖 AI-Assistant Hinweis:**
|
||||
> Testing-Prinzipien für das Meldestelle-Projekt:
|
||||
> - **Test-Pyramide:** 80%+ Unit-Tests, Integrationstests für externe Systeme
|
||||
> - **Testcontainers:** Goldstandard für Infrastruktur-Tests
|
||||
> - **Debug-Logs:** Präfix `[DEBUG_LOG]` für Test-Ausgaben
|
||||
> - **Result-Pattern:** Tests müssen auch Error-Handling validieren
|
||||
|
||||
### Test-Pyramide & Werkzeuge
|
||||
|
||||
#### Unit-Tests (80 %+ Abdeckung)
|
||||
|
||||
Für Domänen- und Anwendungslogik (JUnit 5, MockK).
|
||||
|
||||
```kotlin
|
||||
class MemberServiceTest {
|
||||
private val memberRepository = mockk<MemberRepository>()
|
||||
private val eventPublisher = mockk<EventPublisher>()
|
||||
private val memberService = MemberService(memberRepository, eventPublisher)
|
||||
|
||||
@Test
|
||||
fun `should return Success when member is created successfully`() {
|
||||
// Given
|
||||
val command = CreateMemberCommand(
|
||||
memberId = MemberId.generate(),
|
||||
name = "Max Mustermann",
|
||||
email = "max@example.com"
|
||||
)
|
||||
|
||||
every { memberRepository.save(any()) } returns Result.Success(Unit)
|
||||
every { eventPublisher.publish(any()) } returns Result.Success(Unit)
|
||||
|
||||
// When
|
||||
val result = memberService.createMember(command)
|
||||
|
||||
// Then
|
||||
assertThat(result).isInstanceOf<Result.Success<Unit>>()
|
||||
verify { memberRepository.save(any()) }
|
||||
verify { eventPublisher.publish(ofType<MemberCreatedEvent>()) }
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return Failure when repository save fails`() {
|
||||
// Given
|
||||
val command = CreateMemberCommand(
|
||||
memberId = MemberId.generate(),
|
||||
name = "Max Mustermann",
|
||||
email = "max@example.com"
|
||||
)
|
||||
|
||||
every { memberRepository.save(any()) } returns Result.Failure(RepositoryError.DATABASE_ERROR)
|
||||
|
||||
// When
|
||||
val result = memberService.createMember(command)
|
||||
|
||||
// Then
|
||||
assertThat(result).isInstanceOf<Result.Failure<RepositoryError>>()
|
||||
verify { memberRepository.save(any()) }
|
||||
verify(exactly = 0) { eventPublisher.publish(any()) }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Integrationstests
|
||||
|
||||
Decken alle Repository-Implementierungen und externen Integrationen ab.
|
||||
|
||||
```kotlin
|
||||
@Testcontainers
|
||||
class MemberRepositoryIntegrationTest {
|
||||
|
||||
@Container
|
||||
private val postgresContainer = PostgreSQLContainer("postgres:16-alpine")
|
||||
.withDatabaseName("testdb")
|
||||
.withUsername("test")
|
||||
.withPassword("test")
|
||||
|
||||
private lateinit var memberRepository: MemberRepository
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
val dataSource = HikariDataSource().apply {
|
||||
jdbcUrl = postgresContainer.jdbcUrl
|
||||
username = postgresContainer.username
|
||||
password = postgresContainer.password
|
||||
}
|
||||
|
||||
// Run migrations
|
||||
Flyway.configure()
|
||||
.dataSource(dataSource)
|
||||
.locations("db/migration")
|
||||
.load()
|
||||
.migrate()
|
||||
|
||||
memberRepository = PostgresMemberRepository(dataSource)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should save and retrieve member successfully`() {
|
||||
// Given
|
||||
val member = Member(
|
||||
id = MemberId.generate(),
|
||||
name = "Integration Test Member",
|
||||
email = "integration@test.com"
|
||||
)
|
||||
|
||||
// When
|
||||
val saveResult = runBlocking { memberRepository.save(member) }
|
||||
val findResult = runBlocking { memberRepository.findById(member.id) }
|
||||
|
||||
// Then
|
||||
assertThat(saveResult).isInstanceOf<Result.Success<Unit>>()
|
||||
assertThat(findResult).isInstanceOf<Result.Success<Member?>>()
|
||||
|
||||
val retrievedMember = (findResult as Result.Success).value
|
||||
assertThat(retrievedMember?.id).isEqualTo(member.id)
|
||||
assertThat(retrievedMember?.name).isEqualTo(member.name)
|
||||
assertThat(retrievedMember?.email).isEqualTo(member.email)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Testcontainers als Goldstandard
|
||||
|
||||
Jede Interaktion mit externer Infrastruktur (DB, Cache, Broker) **muss** mit **Testcontainers** getestet werden.
|
||||
|
||||
```kotlin
|
||||
@Testcontainers
|
||||
class EventStoreIntegrationTest {
|
||||
|
||||
companion object {
|
||||
@Container
|
||||
@JvmStatic
|
||||
private val redisContainer = GenericContainer<Nothing>("redis:7-alpine")
|
||||
.withExposedPorts(6379)
|
||||
|
||||
@Container
|
||||
@JvmStatic
|
||||
private val kafkaContainer = KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should store and retrieve events from Redis`() {
|
||||
println("[DEBUG_LOG] Testing Redis event storage")
|
||||
|
||||
// Given
|
||||
val eventStore = RedisEventStore(
|
||||
redisHost = redisContainer.host,
|
||||
redisPort = redisContainer.getMappedPort(6379)
|
||||
)
|
||||
|
||||
val event = MemberCreatedEvent(
|
||||
memberId = MemberId.generate(),
|
||||
name = "Test Member",
|
||||
timestamp = Instant.now()
|
||||
)
|
||||
|
||||
// When
|
||||
val storeResult = runBlocking { eventStore.store(event) }
|
||||
val retrieveResult = runBlocking { eventStore.getEvents(event.memberId) }
|
||||
|
||||
// Then
|
||||
assertThat(storeResult).isInstanceOf<Result.Success<Unit>>()
|
||||
assertThat(retrieveResult).isInstanceOf<Result.Success<List<DomainEvent>>>()
|
||||
|
||||
val events = (retrieveResult as Result.Success).value
|
||||
assertThat(events).hasSize(1)
|
||||
assertThat(events.first()).isInstanceOf<MemberCreatedEvent>()
|
||||
|
||||
println("[DEBUG_LOG] Successfully stored and retrieved ${events.size} events")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Debugging in Tests
|
||||
|
||||
Debug-Ausgaben im Test-Code müssen mit `[DEBUG_LOG]` beginnen, um sie leicht identifizieren und filtern zu können.
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `should handle complex business scenario`() {
|
||||
println("[DEBUG_LOG] Starting complex business scenario test")
|
||||
|
||||
// Test implementation
|
||||
|
||||
println("[DEBUG_LOG] Member created with ID: ${member.id}")
|
||||
println("[DEBUG_LOG] Published ${events.size} domain events")
|
||||
println("[DEBUG_LOG] Test completed successfully")
|
||||
}
|
||||
```
|
||||
|
||||
## 🎯 AI-Assistenten: Testing-Schnellreferenz
|
||||
|
||||
### Test-Kategorien und Werkzeuge
|
||||
|
||||
| Test-Typ | Coverage-Ziel | Werkzeuge | Verwendung |
|
||||
|----------|---------------|-----------|------------|
|
||||
| Unit-Tests | 80%+ | JUnit 5, MockK, AssertJ | Domänen- & Anwendungslogik |
|
||||
| Integrationstests | Alle Repositories | Testcontainers, JUnit 5 | Externe Integrationen |
|
||||
| End-to-End Tests | Kritische User-Journeys | Testcontainers, REST Assured | Vollständige Workflows |
|
||||
|
||||
### Testcontainer-Konfiguration
|
||||
|
||||
#### PostgreSQL
|
||||
```kotlin
|
||||
@Container
|
||||
private val postgresContainer = PostgreSQLContainer("postgres:16-alpine")
|
||||
.withDatabaseName("testdb")
|
||||
.withUsername("test")
|
||||
.withPassword("test")
|
||||
.withInitScript("test-data.sql")
|
||||
```
|
||||
|
||||
#### Redis
|
||||
```kotlin
|
||||
@Container
|
||||
private val redisContainer = GenericContainer<Nothing>("redis:7-alpine")
|
||||
.withExposedPorts(6379)
|
||||
.withCommand("redis-server", "--appendonly", "yes")
|
||||
```
|
||||
|
||||
#### Kafka
|
||||
```kotlin
|
||||
@Container
|
||||
private val kafkaContainer = KafkaContainer(DockerImageName.parse("confluentinc/cp-kafka:7.4.0"))
|
||||
.withEnv("KAFKA_AUTO_CREATE_TOPICS_ENABLE", "true")
|
||||
```
|
||||
|
||||
#### Keycloak
|
||||
```kotlin
|
||||
@Container
|
||||
private val keycloakContainer = KeycloakContainer("quay.io/keycloak/keycloak:26.0.7")
|
||||
.withRealmImportFile("test-realm.json")
|
||||
.withAdminUsername("admin")
|
||||
.withAdminPassword("admin")
|
||||
```
|
||||
|
||||
### Test-Patterns für Result-Handling
|
||||
|
||||
```kotlin
|
||||
// Success-Case testen
|
||||
@Test
|
||||
fun `should return Success when operation succeeds`() {
|
||||
// Given
|
||||
every { dependency.operation() } returns Result.Success(expectedValue)
|
||||
|
||||
// When
|
||||
val result = serviceUnderTest.performOperation()
|
||||
|
||||
// Then
|
||||
assertThat(result).isInstanceOf<Result.Success<ExpectedType>>()
|
||||
assertThat((result as Result.Success).value).isEqualTo(expectedValue)
|
||||
}
|
||||
|
||||
// Failure-Case testen
|
||||
@Test
|
||||
fun `should return Failure when dependency fails`() {
|
||||
// Given
|
||||
every { dependency.operation() } returns Result.Failure(ExpectedError.SOME_ERROR)
|
||||
|
||||
// When
|
||||
val result = serviceUnderTest.performOperation()
|
||||
|
||||
// Then
|
||||
assertThat(result).isInstanceOf<Result.Failure<ExpectedError>>()
|
||||
assertThat((result as Result.Failure).error).isEqualTo(ExpectedError.SOME_ERROR)
|
||||
}
|
||||
```
|
||||
|
||||
### Mock-Setup für Services
|
||||
|
||||
```kotlin
|
||||
class ServiceTest {
|
||||
private val repository = mockk<Repository>()
|
||||
private val eventPublisher = mockk<EventPublisher>()
|
||||
private val externalService = mockk<ExternalService>()
|
||||
|
||||
private val serviceUnderTest = Service(repository, eventPublisher, externalService)
|
||||
|
||||
@BeforeEach
|
||||
fun setup() {
|
||||
clearAllMocks()
|
||||
|
||||
// Default mocks
|
||||
every { eventPublisher.publish(any()) } returns Result.Success(Unit)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun cleanup() {
|
||||
confirmVerified(repository, eventPublisher, externalService)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Testdaten-Builder
|
||||
|
||||
```kotlin
|
||||
class MemberTestDataBuilder {
|
||||
private var id: MemberId = MemberId.generate()
|
||||
private var name: String = "Test Member"
|
||||
private var email: String = "test@example.com"
|
||||
private var status: MemberStatus = MemberStatus.ACTIVE
|
||||
|
||||
fun withId(id: MemberId) = apply { this.id = id }
|
||||
fun withName(name: String) = apply { this.name = name }
|
||||
fun withEmail(email: String) = apply { this.email = email }
|
||||
fun withStatus(status: MemberStatus) = apply { this.status = status }
|
||||
|
||||
fun build() = Member(
|
||||
id = id,
|
||||
name = name,
|
||||
email = email,
|
||||
status = status
|
||||
)
|
||||
}
|
||||
|
||||
// Verwendung in Tests
|
||||
@Test
|
||||
fun `should validate member data`() {
|
||||
val member = MemberTestDataBuilder()
|
||||
.withName("Max Mustermann")
|
||||
.withEmail("max@meldestelle.at")
|
||||
.withStatus(MemberStatus.PENDING)
|
||||
.build()
|
||||
|
||||
// Test implementation
|
||||
}
|
||||
```
|
||||
|
||||
### Performance-Tests
|
||||
|
||||
```kotlin
|
||||
@Test
|
||||
fun `should handle high load efficiently`() {
|
||||
println("[DEBUG_LOG] Starting performance test with 1000 concurrent operations")
|
||||
|
||||
val operations = (1..1000).map {
|
||||
async {
|
||||
serviceUnderTest.performOperation(
|
||||
TestCommand(id = MemberId.generate())
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val results = runBlocking {
|
||||
operations.awaitAll()
|
||||
}
|
||||
|
||||
val successCount = results.count { it is Result.Success }
|
||||
val failureCount = results.count { it is Result.Failure }
|
||||
|
||||
println("[DEBUG_LOG] Performance test completed: $successCount successes, $failureCount failures")
|
||||
|
||||
assertThat(successCount).isGreaterThan(950) // 95% success rate minimum
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Navigation:**
|
||||
- [Master-Guideline](../master-guideline.md) - Übergeordnete Projektrichtlinien
|
||||
- [Coding-Standards](./coding-standards.md) - Code-Qualitätsstandards
|
||||
- [Documentation-Standards](./documentation-standards.md) - Dokumentationsrichtlinien
|
||||
- [Architecture-Principles](./architecture-principles.md) - Architektur-Grundsätze
|
||||
Reference in New Issue
Block a user