.NET: Architekturtests mit ArchUnitNet

5 Minuten zum Lesen
.NET: Architekturtests mit ArchUnitNet
Architekturtests mit ArchUnitNet helfen, die Einhaltung von Designprinzipien in .NET-Projekten sicherzustellen. Sie prüfen Schichtentrennung, Namenskonventionen und vermeiden zyklische Abhängigkeiten, um langfristig sauberen und wartbaren Code zu gewährleisten.

Zu Beginn eines Softwareprojekts wird die Architektur oft sorgfältig geplant. Man überlegt sich, wie der Code in Klassenbibliotheken aufgeteilt wird, welche Prinzipien wie Separation of Concerns oder SOLID eingehalten werden sollen und wie die einzelnen Komponenten miteinander interagieren. Diese Planung bildet die Grundlage für eine saubere und wartbare Codebasis. Doch mit der Zeit, neuen Entwickler:innen im Team und wachsender Codebasis schleichen sich häufig Verstöße gegen diese Prinzipien ein. Abhängigkeiten entstehen, wo keine sein sollten, und Klassen übernehmen zu viele Aufgaben.

Selbst mit guter Dokumentation und regelmäßigen Code Reviews können Quick-Fixes, Zeitdruck oder fehlendes Wissen über das Gesamtsystem dazu führen, dass die ursprüngliche Architektur verwässert wird. Wie kann man diese Herausforderungen bewältigen und sicherstellen, dass die Architektur langfristig sauber bleibt? Eine Lösung sind Architekturtests.

Was sind Architekturtests?

Architekturtests sind automatisierte Prüfungen, die sicherstellen, dass die Implementierung den definierten Architekturvorgaben entspricht. Sie helfen dabei, Schichtentrennungen einzuhalten, Namenskonventionen zu überprüfen und Abhängigkeiten korrekt zu definieren. Im Gegensatz zu Unit- oder Integrationstests, die die Funktionalität des Codes prüfen, konzentrieren sich Architekturtests auf die Struktur des Codes und die Einhaltung von Designprinzipien.

Ein Beispiel: Wenn in einem Projekt eine klare Trennung zwischen der Service-Schicht und der UI-Schicht definiert wurde, können Architekturtests sicherstellen, dass keine Klasse aus der Service-Schicht direkt auf Klassen der UI-Schicht zugreift. Solche Tests sind besonders hilfreich, um technische Schulden zu vermeiden und die langfristige Wartbarkeit des Codes zu gewährleisten.

Einführung in ArchUnitNet

Für .NET-Projekte bietet sich ArchUnitNet als Tool für Architekturtests an. Es handelt sich um eine .NET-Implementierung der bekannten ArchUnit-Bibliothek aus der Java-Welt. ArchUnitNet ermöglicht es, Architekturregeln in einer Fluent API zu definieren und diese gegen den Code zu prüfen. Dadurch können Entwickler:innen sicherstellen, dass die Architekturvorgaben eingehalten werden, ohne dass dies manuell überprüft werden muss.

Der erste Schritt bei der Verwendung von ArchUnitNet ist das Laden der relevanten Assemblies. Dies kann über Reflection erfolgen, um alle Projekte mit einem bestimmten Namensschema automatisch einzubinden:

using ArchUnitNET.Domain;
using ArchUnitNET.Loader;

var architecture = new ArchLoader().LoadAssemblies("MyProject.*").Build();

Assemblies können manuell referenziert werden, indem jedes Projekt im Architekturtest-Projekt eingebunden wird. Dies ist jedoch aufwendig. Eine bessere Lösung ist, alle relevanten Projekte zu referenzieren und per Reflection Assemblies aus dem aktuellen Verzeichnis zu laden. Um Third-Party-Libraries auszuschließen, empfiehlt sich eine Namenskonvention wie MyProject.*.

var architecture = new ArchLoader()
    .LoadFilteredDirectory(AppDomain.CurrentDomain.BaseDirectory, "MyProject.*")
    .Build();

Alternativ kann das Microsoft Traversal SDK genutzt werden, um per Wildcard auf Projekte zu verweisen. Eine Directory.Build.props-Datei könnte so aussehen:

<Project Sdk="Microsoft.Build.Traversal">
    <ItemGroup>
        <ProjectReference Include="src/**/MyProject.*.csproj" />
    </ItemGroup>
</Project>

Damit werden alle Projekte mit dem Präfix MyProject. automatisch referenziert und gebaut. Sobald die Assemblies geladen sind, können Architekturregeln definiert und geprüft werden.

Beispiele für Architekturregeln

Schichtentrennung

Eine der häufigsten Regeln in Softwareprojekten ist die Sicherstellung einer klaren Schichtentrennung. Mit ArchUnitNet kann beispielsweise geprüft werden, dass Klassen in der Service-Schicht nicht direkt auf Klassen der UI-Schicht zugreifen:

using static ArchUnitNET.Fluent.ArchRuleDefinition;

var noDependencyFromServiceToUI =
    Classes().That().ResideInNamespace("MyProject.Service")
    .Should().NotDependOnAny(Classes().That().ResideInNamespace("MyProject.UI"));

noDependencyFromServiceToUI.Check(architecture);

Diese Regel verhindert, dass die Service-Schicht von der UI-Schicht abhängig wird, was die Modularität und Testbarkeit des Codes verbessert.

Namenskonventionen

Ein weiteres Beispiel ist die Überprüfung von Namenskonventionen. In vielen Projekten wird beispielsweise festgelegt, dass Data Transfer Objects (DTOs) immer mit dem Suffix Dto enden sollen. Mit ArchUnitNet lässt sich dies wie folgt prüfen:

var dtosShouldHaveCorrectSuffix =
    Classes().That().ResideInNamespace("MyProject.DTOs")
    .Should().HaveNameEndingWith("Dto");

dtosShouldHaveCorrectSuffix.Check(architecture);

Solche Regeln helfen dabei, Konsistenz im Code zu gewährleisten und die Lesbarkeit zu verbessern.

Zyklische Abhängigkeiten

Zyklische Abhängigkeiten zwischen Modulen oder Klassen können die Wartbarkeit des Codes erheblich erschweren. ArchUnitNet bietet eine einfache Möglichkeit, solche Abhängigkeiten zu erkennen und zu verhindern:

var noCyclicDependencies = Slices().Matching("MyProject.(*)").Should().BeFreeOfCycles();
noCyclicDependencies.Check(architecture);

Diese Regel stellt sicher, dass keine Module in einer zyklischen Abhängigkeit zueinander stehen, was die Architektur stabiler und übersichtlicher macht.

Logging-Prüfung

Zu guter Letzt können wir auch die Einhaltung von Logging-Praktiken prüfen, indem wir beispielsweise Aufrufe von Console.WriteLine verbieten. Solche Regelbrüche lassen sich mit Architekturtests validieren:

var noConsoleWriteLine =
    Methods().That().HaveDependencyInMethodBodyTo(typeof(Console).GetMethod("WriteLine"))
        .Should().NotExist();

noConsoleWriteLine.Check(architecture);

Alternativ könnte ein Code Analyzer die bessere Wahl sein, da er Regelverletzungen direkt im Editor anzeigt und passende Code-Fixes vorschlägt. In Zeiten von Tools wie Copilot lassen sich solche Analyzer einfach selbst schreiben.

Integration mit PlantUML

Hat man einmal seine Schichten und Komponenten sauber definiert, kann man diese direkt als Dokumentation in Form eines PlantUML-Diagramms exportieren. ArchUnitNet bietet hierfür eine Exportfunktion:

PlantUmlDefinition.ComponentDiagram().WithDependenciesFromSlices(sliceRule.GetObjects(arch)).WriteToFile("architecture.puml");

Auch die andere Richtung ist möglich: Wer seine Abhängigkeiten vorab als PlantUML-Komponentendiagramm definiert, kann diese in ArchUnitNet importieren und gegen den Code prüfen:

String myDiagram = "./Resources/my-diagram.puml";
IArchRule someRule = Types().Should().AdhereToPlantUmlDiagram(myDiagram);
someRule.Check(Architecture);

So bleibt die Dokumentation immer synchron mit dem Code.

Vorteile von Architekturtests

Architekturtests bieten zahlreiche Vorteile. Sie dokumentieren die Architektur nicht nur, sondern machen sie auch direkt testbar. Dadurch bleibt die Dokumentation immer aktuell und kann nicht von der Implementierung abweichen. Außerdem helfen Architekturtests, technische Schulden zu vermeiden, indem sie Verstöße gegen die Architektur frühzeitig erkennen und beheben.

Ein weiterer Vorteil ist die Möglichkeit, Architekturregeln direkt im Code zu definieren. Dies erleichtert die Zusammenarbeit im Team, da alle Entwickler:innen die gleichen Regeln einhalten müssen. Zudem können Architekturtests in den Build-Prozess integriert werden, sodass Verstöße automatisch erkannt werden, bevor der Code in die Produktion gelangt.

Herausforderungen bei der Einführung

Trotz ihrer Vorteile gibt es auch Herausforderungen bei der Einführung von Architekturtests. Die Tests müssen regelmäßig gepflegt und an Änderungen in der Architektur angepasst werden. Außerdem erfordert die Definition sinnvoller Regeln ein tiefes Verständnis der Architektur und der Anforderungen des Projekts. Es ist wichtig, dass Ausnahmen wirklich die Ausnahme bleiben und nicht zur Regel werden.

Fazit

Architekturtests sind ein mächtiges Werkzeug, um die Qualität und Wartbarkeit von Softwareprojekten langfristig zu sichern. Mit Tools wie ArchUnitNet lassen sich solche Tests einfach implementieren und in den Entwicklungsprozess integrieren. Sie helfen dabei, Verstöße gegen die Architektur frühzeitig zu erkennen und technische Schulden zu vermeiden.

Wie definierst und überprüfst du die Architektur in deinen Projekten? Teile deine Ansätze und Erfahrungen mit uns auf LinkedIn.

Florian Bader

Florian Bader

Florian ist Solution Architect und Microsoft Most Valuable Professional für Azure IoT mit langjähriger Erfahrung im Bereich DevOps, Cloud und Digitalisierung. Er unterstützt Unternehmen dabei, effiziente und effektive Lösungen zu entwickeln, die ihre digitalen Projekte nachhaltig zum Erfolg führen.

Verwandte Beiträge

Entdecken Sie weitere Artikel, die ähnliche Themen behandeln und vertiefen Sie Ihr Wissen. Diese Beiträge wurden basierend auf gemeinsamen Tags und Veröffentlichungsdaten ausgewählt. Viel Spaß beim Weiterlesen!