feat: add architecture tests for enforcing module boundaries

Integrated a new `:platform:architecture-tests` module using ArchUnit to enforce backend and frontend architecture rules. Configured explicit dependencies to all relevant modules and implemented rules to prevent cross-dependencies between backend services and frontend features. Replaced legacy Gradle-based architecture guards with this robust solution. Updated CI pipeline to include these tests.
This commit is contained in:
2026-01-29 14:28:36 +01:00
parent fd7eba3589
commit 5feb42d973
8 changed files with 182 additions and 112 deletions
+26
View File
@@ -0,0 +1,26 @@
# Architecture Tests
Dieses Modul ist der zentrale "Architektur-Wächter" des gesamten Projekts. Es verwendet [ArchUnit](https://www.archunit.org/) um sicherzustellen, dass die definierten Architektur-Regeln im Code nicht verletzt werden.
## Zweck
Der Hauptzweck dieses Moduls ist es, die strikte Trennung zwischen den verschiedenen Teilen des Systems technisch zu erzwingen und so die langfristige Wartbarkeit und Skalierbarkeit zu sichern. Dazu gehören Regeln wie:
* **Keine Kreuz-Abhängigkeiten zwischen Backend-Services:** Ein Service darf nicht direkt auf den Code eines anderen Service zugreifen.
* **Keine Kreuz-Abhängigkeiten zwischen Frontend-Features:** Ein Feature darf nicht direkt auf den Code eines anderen Features zugreifen.
* **Einhaltung von Schichten-Architekturen:** z.B. Controller -> Service -> Repository.
Diese Tests laufen als Teil der CI-Pipeline. Ein Build schlägt fehl, wenn eine Regel verletzt wird.
## Wie es funktioniert
1. **Abhängigkeiten:** Die `build.gradle.kts` dieses Moduls deklariert explizite `implementation`-Abhängigkeiten zu **allen** baubaren Modulen des Projekts, deren Architektur verifiziert werden soll.
2. **Test-Klassen:** Die eigentlichen Regeln sind als JUnit 5-Tests in den `src/test/kotlin`-Verzeichnissen implementiert (z.B. `BackendArchitectureTest.kt`).
3. **Klassen-Import:** Die Tests verwenden die `@AnalyzeClasses`-Annotation von ArchUnit, um die Klassen aus den deklarierten Abhängigkeiten zu scannen.
4. **Regel-Definition:** Innerhalb der Tests werden ArchUnit-Regeln definiert (z.B. mit `slices().should().notDependOnEachOther()`), die auf die importierten Klassen angewendet werden.
## Wie man neue Regeln hinzufügt
1. **Abhängigkeit sicherstellen:** Stellen Sie in der `build.gradle.kts` dieses Moduls sicher, dass eine Abhängigkeit zum neuen Modul, das Sie testen wollen, existiert.
2. **Test-Klasse erweitern:** Fügen Sie eine neue `@ArchTest`-Methode zu einer der bestehenden Test-Klassen (`BackendArchitectureTest.kt`, `FrontendArchitectureTest.kt`) hinzu oder erstellen Sie eine neue Test-Klasse für eine neue Kategorie von Regeln.
3. **Regel definieren:** Schreiben Sie Ihre Regel unter Verwendung der ArchUnit-API.
@@ -0,0 +1,49 @@
plugins {
alias(libs.plugins.kotlinJvm)
}
// This module tests the architecture of the entire project.
// It needs explicit dependencies on all modules that contain code to be checked.
dependencies {
// ArchUnit
implementation(libs.archunit.junit5.api)
runtimeOnly(libs.archunit.junit5.engine)
// Standard Kotlin Test-Bibliothek
implementation(libs.kotlin.test)
implementation(libs.kotlin.test.junit5)
// --- ADD ALL MODULES WITH CODE TO BE TESTED HERE ---
// This list must be maintained manually.
// --- CONTRACTS ---
implementation(project(":contracts:ping-api"))
// --- CORE ---
implementation(project(":core:core-domain"))
implementation(project(":core:core-utils"))
// --- BACKEND ---
implementation(project(":backend:services:ping:ping-service"))
implementation(project(":backend:services:entries:entries-api"))
implementation(project(":backend:services:entries:entries-service"))
implementation(project(":backend:services:registry:registry-api"))
implementation(project(":backend:services:registry:registry-domain"))
implementation(project(":backend:services:registry:registry-service"))
// --- FRONTEND ---
implementation(project(":frontend:features:ping-feature"))
implementation(project(":frontend:core:auth"))
implementation(project(":frontend:core:domain"))
implementation(project(":frontend:core:design-system"))
implementation(project(":frontend:core:navigation"))
implementation(project(":frontend:core:network"))
implementation(project(":frontend:core:local-db"))
implementation(project(":frontend:core:sync"))
implementation(project(":frontend:shared"))
implementation(project(":frontend:shells:meldestelle-portal"))
}
tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
@@ -0,0 +1,30 @@
package at.mocode.archtests
import com.tngtech.archunit.core.domain.JavaClasses
import com.tngtech.archunit.junit.AnalyzeClasses
import com.tngtech.archunit.junit.ArchTest
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses
// Scan ALL project classes from the root package
@AnalyzeClasses(packages = ["at.mocode"])
class BackendArchitectureTest {
@ArchTest
fun `service modules should not depend on each other`(importedClasses: JavaClasses) {
val servicePackages = listOf(
"at.mocode.ping..",
"at.mocode.entries.."
// Add other service packages here as they are created
)
for (servicePackage in servicePackages) {
val otherServicePackages = servicePackages.filter { it != servicePackage }.toTypedArray()
if (otherServicePackages.isEmpty()) continue
noClasses()
.that().resideInAPackage(servicePackage)
.should().accessClassesThat().resideInAnyPackage(*otherServicePackages)
.check(importedClasses)
}
}
}
@@ -0,0 +1,20 @@
package at.mocode.archtests
import com.tngtech.archunit.core.domain.JavaClasses
import com.tngtech.archunit.junit.AnalyzeClasses
import com.tngtech.archunit.junit.ArchTest
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices
// Scan ALL project classes from the root package
@AnalyzeClasses(packages = ["at.mocode"])
class FrontendArchitectureTest {
@ArchTest
fun `feature modules should not depend on each other`(importedClasses: JavaClasses) {
// The pattern must match the actual package structure, e.g., 'at.mocode.ping.feature'
slices()
.matching("at.mocode.(*).feature..")
.should().notDependOnEachOther()
.check(importedClasses)
}
}