Architektur Testing mit ArchUnit

Roman Bertolami

Software Architektur ist heutzutage keine Disziplin, die sich primär vor der Entwicklung abspielt. Stattdessen versuchen wir am Anfang ein grobes Bild des Systems zu zeichnen um dieses dann laufend zu verfeinern. Sinnvollerweise halten wir auf diesem Weg die wichtigsten Architekturentscheidungen fest, beispielsweise mittels Architectural Decision Records. Damit können zu einem späteren Zeitpunkt die Gründe für eine Entscheidung erfahren und allenfalls geprüft werden.

Gut ist auch, wenn wir das grobe Architekturbild des Systems mit der aktuellen Implementation automatisch abgleichen. Damit merken wir sofort, wenn sich die Implementation von den Architektur-Ideen entfernt hat und können entweder das eine oder das andere entsprechend anpassen. Diese Änderungen erfolgen dadurch explizit und können zum Zeitpunkt des auftretenden Konflikts mit dem Team diskutiert werden: Stimmt unsere Architektur noch oder ist sie überholt? Lässt sich der Code einfach anpassen, so dass er gemäss der Architektur konform ist?

Zu diesem Zweck verwendeten wir lange eine eigene Lösung. Diese auf JDepend basierende Lösung überprüfte die Abhängigkeiten, die in einem einem CSV File erfasst wurden. Die Lösung war einfach und erfüllte ihren Zweck, die Wartung der Abhängigkeiten im CSV File war aber eher mühsam. Zudem beschränkten sich die Prüfungen auf die Validierung von Package Abhängigkeiten und der Detektion von Zyklen.

ArchUnit

Heute gibt es dazu eine neue Lösung - ArchUnit. Das Testing Framework ist für Java und Kotlin Applikationen eine einfache Alternative zu einer eigenen Lösung. Die Regeln und Abhängigkeiten können direkt im Code geführt werden. Wir erfassen die Regeln als JUnit Tests und können sie dann analog mit allen anderen Unit-Tests prüfen lassen. Dies führt dazu, dass die Tests einfach in die bestehende Testumgebung eingebaut werden können und es keine zusätzlichen Hürden für die Verwendung gibt.

Das Framework besteht aus drei Layern: Core, Lang und Library. Der Core von ArchUnit erlaubt es, Java Bytecode als Java Objekte zu importieren. Mit dem Lang API können auf den importierten Java Objekten Regeln definiert werden, die das Framework dann im Unit Test prüft. Eine Erweiterung stellt die Library API dar. Für häufige Architektur Patterns wie beispielsweise die Onion Architecture stehen hier vorgefertigte Regeln zur Verfügung.

Im Folgenden erläutern wir kurz die wichtigsten Anwendungsfälle für ArchUnit anhand einfacher Beispiele. Der vollständige Code der Beispiele ist auf Github frei verfügbar.

Use Case 1: Abhängigkeit prüfen

Die wohl einfachste Prüfung ist es zu validieren, ob zwischen Packages unerlaubte Abhängigkeiten vorkommen.

Unsere Demo Applikation verfügt über ein Package domain und ein Package service. Dabei dürfen Services jeweils Domänenobjekte verwenden aber Domänenobjekte dürfen umgekehrt keine Services verwenden. Um dies zu prüfen können wir nachfolgende Regeln definieren. Diese stellen sicher, dass die definierten Abhängigkeiten eingehalten werden:

// Klassen in Package domain dürfen nur interne Klassen referenzieren
@ArchTest
static final ArchRule zeroDependencyOfDomain = classes().that()
        .resideInAPackage("..domain..")
        .should().onlyDependOnClassesThat().resideInAnyPackage( "..domain..","java..");

// Nur Services dürfen auf die Domain zugreifen
@ArchTest
static final ArchRule domainOnlyAccessedByService = classes().that()
        .resideInAPackage("..domain..")
        .should().onlyHaveDependentClassesThat()
        .resideInAnyPackage("..service..");

Use Case 2: Zyklen verhindern

Oft ist es wichtig zu erkennen, wann in einer Software zyklische Abhängigkeiten eingebaut werden. Selten sind diese bereits in der Grundidee der Architektur vorgesehen - oft schleichen sie sich jedoch im Laufe der Entwicklung und Wartung der Software ein. Die Library API von ArchUnit bietet hier einfache Mittel, um diese umgehend zu detektieren. Dazu wird die Software in slices aufgeteilt, die wir dann auf Zyklen prüfen können.

So werden im folgenden einfachen Beispiel die Top-Level Packages der Demo-Applikation auf Zyklen geprüft.

@ArchTest
public static final ArchRule noCycles = slices()
        .matching("ch.dsi.archunitdemo.(**)")
        .should().beFreeOfCycles();

Natürlich müssen wir die slices so wählen, dass sie für unsere Applikation sinnvoll sind. Nicht alle Zyklen müssen umgehend behoben werden.

Use Case 3: Prüfen eines Diagramms

Architekturdiagramme helfen, das Verständnis für die Struktur einer Applikation zu verbessern. Leider stimmen die Diagramme, die meist zu Beginn der Entwicklung erstellt werden, oft nicht mehr mit der tatsächlichen Struktur der Software überein. Letztere wird vielfach weiter entwickelt, ohne dass die Diagramme geprüft und angepasst werden.

ArchUnit unterstützt die Prüfung von PlantUML Diagrammen. Dadurch können die wichtigsten Komponenten einer Architektur in einem Diagramm festgehalten werden, bei dem auch stets geprüft wird, ob es noch der aktuellen Implementation entspricht.

Für die Beispiel Applikation könnte ein solches PlantUML Diagramm wie folgt aussehen:

@startuml
    [Domain] <<ch.dsi.archunitdemo.domain..>> as domain
    [Service] <<ch.dsi.archunitdemo.service..>> as service
    service --> domain
@enduml

Visualisiert ergibt dies das folgendes Diagramm:

diagram

Das Diagramm wird dann in den ArchUnit Test geladen und die Implementation wird dagegen geprüft:

static URL myDiagram = ArchitectureTest.class.getResource("/component-diagram.puml");

@ArchTest
static final ArchRule conformToComponentDiagram = classes()
        .should(adhereToPlantUmlDiagram(myDiagram, consideringOnlyDependenciesInDiagram()));

Das “Zeichnen” von Architekturdiagrammen mit PlantUML ist vielleicht nicht ganz so praktisch wie mit einem WYSIWYG Tool, dafür lassen sich die Diagramme einfach mit git versionieren und durch die ArchUnit Prüfung sind sie immer auf dem aktuellen Stand.

Fazit

Mit ArchUnit lassen sich im Team erarbeitete Architektur-Regeln relativ einfach überprüfen. Wie immer gilt es dabei einen Mittelweg zu finden, indem wir möglichst wenige Regeln definieren, die möglichst viel Wirkung entfachen.

Ein grosser Vorteil von ArchUnit ist, dass es in den normalen Test Zyklus eingebunden ist. Dadurch wird es nicht als externes Architektur-Tool wahrgenommen und wird vom Entwicklungsteam akzeptiert. Der schnelle Feedback-Zyklus führt zudem dazu, dass das Architekturbild und die Implementation synchron bleiben.

Der in diesem Beitrag dargestellte Code befindet sich frei verfügbar auf Github.

Möchten Sie eine bestehende Architektur prüfen und weiterentwickeln? Wir beraten Sie gerne.