Erfahren Sie, wie Sie mit ArchUnit Architekturregeln kontinuierlich testen können. Inklusive Tipps für die Praxis und ein Cheat Sheet zum Download.
Die Architektur einer Anwendung beschreibt unter anderem ihre Struktur sowie Konventionen für die gemeinsame Entwicklung. Entsprechende Architekturregeln zahlen auf die Nachhaltigkeit, insbesondere durch eine bessere Verständlichkeit und damit Wartbarkeit der Software ein.
Leider ist die Einhaltung besagter Regeln mit einem gewissen Prüfaufwand verbunden, der in der täglichen Entwicklung durch manuelle Reviews oft kaum zu leisten ist.
Also warum nicht Tests für Architekturregeln, die genauso leicht wie die heutzutage etablierten Unit-Tests, sowohl bei der lokalen Entwicklung als auch im zentralen Integrationsserver, automatisiert ausgeführt werden können?
In diesem Artikel stellen wir mit ArchUnit, ein Werkzeug vor, das wir für eben solche Tests erfolgreich in Java-Projekten verwenden und geben Tipps aus dem Praxis-Einsatz.
Architektur, Konventionen und Regelwerke
Der Begriff Software-Architektur ist nicht eineindeutig definiert, vielmehr gibt es viele verschiedene Definitionen. Eine recht grundlegende Eigenschaft, die nahezu alle Definitionen gemein haben, ist, dass Software-Architektur Strukturen und Konventionen beschreibt, die über die Syntax der verwendeten Programmiersprache(n) hinausgeht. Beispielsweise beschreibt sie, aus welchen Komponenten eine Software besteht oder welche Abhängigkeiten und Namenskonventionen bei der Entwicklung einzuhalten sind. Gerade der bewusste Umgang mit externen Abhängigkeiten kann durch offene Komponenten-Bibliotheken wie Maven Central auch schnell mal zu einer Herausforderung werden. Ein noch viel simpleres Beispiel, das wir in der Praxis immer wieder antreffen, ist die gleichzeitige Verwendung verschiedener Logging-APIs in einem Projekt, die durch die Autovervollständigung der lokalen Entwicklungsumgebung eingefügt werden.
Der Unit-Test-Ansatz für Architekturen
Architekturen werden heutzutage von Entwicklungsteams nicht nur umgesetzt, sondern oftmals sogar mitgestaltet. Entsprechend hilfreich ist ein schnelles Feedback für alle Entwickler:innen, ob alle Architekturregeln eingehalten wurden. In heißen Entwicklungsphasen kann es schnell einmal passieren, dass eine Regel unbeabsichtigt verletzt wird. Diese Verletzung rein durch manuelle Prüfungen, beispielsweise im Rahmen von Code-Reviews, festzustellen, verlangt einen hohen Aufwand, den man gerne durch eine entsprechende Automatisierung sparen würde.
Automatisierte Unit-Tests gehören heutzutage zum Standardwerkzeugkasten in der Software-Entwicklung und vermutlich allen Java-Entwickler:innen ist JUnit als De-facto-Standard hierfür bekannt. Genauso etabliert ist es, die entwickelten Tests sowohl lokal, als auch in einer zentralen Integrations- bzw. Testumgebung ausführen zu lassen. Da liegt es nahe eine solche Möglichkeit auch für die Prüfung von Architekturregeln zu nutzen.
ArchUnit ist ein OpenSource-Werkzeug für die Entwicklung von Unit-Tests für Java-Anwendungen, das wir bei der viadee in vielen Entwicklungsteams und mit Architekt:innen selbst schon in vielen Projekten gewinnbringend eingesetzt haben. Wird bereits JUnit in der Version 4 oder 5 eingesetzt, so lässt sich auch ArchUnit leicht hinzufügen und gemeinsam mit allen anderen Unit-Tests ausführen – sei es lokal oder auf einem automatisierten Integrationsserver wie Jenkins. Die Ergebnisse der ArchUnit-Tests werden dann automatisch in den existierenden Testberichten mit aufgeführt.
ArchUnit Quickstart
Der Start mit ArchUnit ist denkbar einfach und innerhalb von wenigen Minuten gewinnt man ein erstes Gefühl für den Nutzen. Zunächst sollte ein einfaches Java Projekt in der Entwicklungsumgebung der Wahl angelegt und idealerweise direkt eine Maven- oder Gradle- Unterstützung aktiviert werden.
Einbindung
Dank Maven oder Gradle, ist die Einbindung mit wenigen Zeilen möglich. Für den Start reicht es beispielsweise die JUnit-5-Unterstützung von ArchUnit zu referenzieren:
Maven
Gradle
Nach einem Maven- bzw. Gradle-Update auf dem Projekt kann es losgehen.
Der erste Test
Ein einfacher Test dafür, dass von der Controller-Schicht nicht direkt auf die Persistenz-Schicht zugegriffen wird, kann wie folgt aussehen:
Zunächst wird der Programmcode eines oder mehrerer Packages importiert. Im nächsten Schritt wird eine Regel definiert, dass Klassen innerhalb von controller-Packages nicht auf Klassen innerhalb von persistence-Packages zugreifen dürfen. Die Zwei-Punkte-Schreibweise („..“) stellt hierbei eine Wildcard für beliebige Paketnamen bzw. –pfade dar. Die finale because-Anweisung unterstützt die Fehleranalyse, wenn ein Test anschlägt. Als letztes wird die Regel auf die importierten Klassen angewendet.
Wenn hier der Begriff „classes“ verwendet wird, so steht er synonym auch für andere Java-Artefakte wie Interfaces, Enums oder Methoden.
Ausführung und Fehleranalyse
Der soeben definierte Test kann nun wie jeder andere Unit-Test in der IDE (bspw. in Eclipse per Rechtsklick über „Run As -> JUnit Test“) oder beim Maven Build (bspw. mittels „mvn clean test“) ausgeführt werden.
Schlägt der Test an, wird nicht nur ein Fehler angezeigt, sondern ArchUnit liefert im sogenannten „Failure Trace“ von JUnit auch eine Erklärung wie im folgenden Beispiel mit:
Als erstes wird eine Regelverletzung („Architecture Violation“) gemeldet. Die Beschreibung wird automatisch aus den verwendeten ArchUnit-Methoden generiert und mit der Begründung im because-Aufruf sowie der Anzahl der Verletzungen kombiniert („3 times“). Nachfolgend gibt es zu jeder der identifizierten Verletzungen („Constructor“, „Field“, „Method“) einen Block mit einem Hinweis, wo die Regelverletzung aufgetreten ist (z. B. „has parameter of type“). An den entsprechenden Stellen kann dann nachgearbeitet werden.
Kurzschreibweise
ArchUnit liefert auch einen eigenen Testrunner, der über @AnalyzeClasses an der Testklasse annotiert werden kann. Hierbei können Optionen, wie zum Beispiel ein Package-Filter mitgegeben werden. Die auszuführenden Testmethoden werden als statische Variable vom Typ ArchRule definiert und mit der @ArchTest-Annotation gekennzeichnet. Die obige Testklasse kann dann wie folgt vereinfacht werden:
Prüfungsarten und -Beispiele
ArchUnit unterstützt unterschiedliche Arten von Prüfungen, die auf Basis des kompilierten Java Codes möglich sind. Hierzu gehören beispielsweise die Prüfung von Referenzen oder Vererbungshierarchien, bis hin zu Namenskonventionen, Zugriff-Modifiern oder Paketzugehörigkeiten.
Schichten-Architekturen
Eine recht typische Architekturregel ist die Trennung von Anwendungsschichten (Layern) und deren klar geregelten Abhängigkeitsbeziehungen. Vermischt mit Code-Vererbung kann eine entsprechende Regelverletzung in einem Review auch mal übersehen werden. ArchUnit bietet hier eine explizite Unterstützung, die es ermöglicht, Schichten durch Package-Regeln zu definieren und ihre Zugriffe zu prüfen.
Folgendes Diagramm zeigt valide (grüne) und unerwünschte (rote) Referenzen zwischen den Anwendungsschichten:
Folgende Architekturprüfung hilft dabei die unerwünschten Zugriffe zu identifizieren:
Nachdem die Layer spezifiziert wurden (“Controller”,”Service”,”Adapter”) wird angegeben, welche Zugriffe auf einen Layer erlaubt sind. Alle anderen Zugriffe werden direkt als Regelverstöße gemeldet.
Hexagonale Architekturen
Neben der klassischen Schichtenarchitektur hat in den letzten Jahren auch das Konzept der Hexagonalen Architekturen – auch unter „Ports & Adapters“ bekannt – an Zuspruch gewonnen. Gleiches gilt für die darauf aufbauende Onion-Architecture. Grundidee ist die eigentliche Fachlichkeit einer Anwendung in ihren Kern zu stellen und Infrastruktur sowie alle externen Verbindungen zu kapseln, um sie voneinander zu entkoppeln.
Abhängigkeiten sind in Hexagonalen Architekturen wie in der folgenden Abbildung nur von außen nach innen gewünscht:
ArchUnit bietet hierfür inzwischen eine explizite Unterstützung angelehnt an den Begriff der Onion-Architecture, die wie im folgenden Code-Beispiel genutzt werden kann:
Wie im Code-Beispiel gezeigt, kennt ArchUnit die Schichten Domain Models und Services, Application Services und beliebige Adapter. Die dazugehörige Architekturregel weiß, welche Referenzen zwischen den Schichten erlaubt sind. Die Option „withOptionalLayers“ erlaubt, dass Layer der Onion-Architecture nicht zwingend gefüllt sein müssen, solange die Abhängigkeitsrichtung erfüllt bleibt.
Externe Abhängigkeiten
Um einen Wildwuchs verwendeter Bibliotheken und damit auch verschiedene Implementierungen für die gleichen Herausforderungen zu vermeiden, kann mit ArchUnit auch die Verwendung externer Bibliotheken geprüft werden.
Das folgende Beispiel prüft, dass nur die neue java.time.LocalDate API und nicht mehr die ältere java.util.Date API verwendet wird:
Namenskonventionen
Namenskonventionen tragen oft zu einer besseren Verständlichkeit und Orientierung im Programmcode bei. ArchUnit bietet hierfür die Möglichkeit, die Namen von Java-Elementen zu prüfen.
Das folgende Beispiel definiert eine Regel, die prüft, dass die Namen aller Spring RestController Implementierungen auf „Controller“ enden und in einem Package unterhalb eines Packages namens controller liegen sollen.
ArchUnit: Architektur und Erweiterbarkeit
Im vorhergehenden Abschnitt wurden einige typische Regeln exemplarisch vorgestellt. Generell stammen ArchUnit-Regeln aus drei möglichen Quellen:
- Vorgefertigte Regeln
- Individuelle Regeln
- Unternehmensweite und produktübergreifende Regeln
Vorgefertigte Regeln werden als vordefinierte ArchRule Deklarationen direkt von ArchUnit mitgeliefert und können einfach im eigenen Testcode referenziert werden. Beispielsweise wird so im folgenden Beispiel geprüft, dass keine generischen, nicht weiter spezifizierten Exceptions verwendet werden:
Viele dieser Tests setzen wir in nahezu allen Projekten ein, bei denen ArchUnit zum Einsatz kommt. Sie sind als statische Variablen unterhalb des Packages com.tngtech.archunit.library deklariert.
Individuelle Regeln sind solche, die für ein Projekt oder Produkt definiert werden und entsprechend als eigene Unit-Tests implementiert werden. Hierfür bietet ArchUnit eine umfangreiche API an, von der wir in den obigen Beispielen nur einen kleinen Ausschnitt gezeigt haben.
Unternehmensweite und produktübergreifende Regeln können genauso wie die von ArchUnit mitgelieferten Regeln vordefiniert und als Paket bereitgestellt werden. Ein einfaches Beispiel wäre, dass der eigene Anwendungscode unterhalb eines Basispakets des Unternehmens liegen müssen (z. B. „de.viadee…“).
ArchUnit Schichtenarchitektur
Einen großen Anteil an der Flexibilität, die ArchUnit auch im Sinne der Erweiterbarkeit und Wiederverwendbarkeit mitbringt, ist seine Schichtenarchitektur. Wie im folgenden Diagramm dargestellt, besteht sie aus drei Schichten, die alle eine gewisse Erweiterbarkeit zulassen:
Core, Lang und Library.
In der grundlegenden Core-Schicht steckt alles Notwendige, um Java-Code importieren und mit seinen Elementen arbeiten zu können. Die Schnittstelle dieser Schicht erinnert an die Java Reflection API ist aber deutlich einfacher in der Handhabung.
Die Lang-Schicht bietet die Infrastruktur zur Regel-Definition und -Prüfung. Hier liegen die Methoden zur eigentlichen Test-Implementierung, wie zum Beispiel should() oder dependOnClassesThat().
In der obersten, der Library-Schicht, stellt ArchUnit vorgefertigte Regeln, wie die Prüfung auf generische Exceptions oder Java-Logging sowie die Unterstützung spezifischer Architekturtypen (LayeredArchitecture oder OnionArchitecture), bereit. Darüber befinden sich hier Hilfsmittel, wie eine Integration mit PlantUML und ein Freezing-Feature für den Umgang mit bestehenden Anwendungen, das weiter unten näher beschrieben wird.
Praktische Wiederverwendbarkeit und Erweiterbarkeit
Werden im eigenen Unternehmen produkt- oder anwendungsübergreifende Regeln übergreifend verwendet, so können diese genauso wie in der Library-Schicht von ArchUnit als statische Regeln implementiert und beispielsweise als Maven Modul verschiedenen Entwicklungsprojekten bereitgestellt werden. Hier ein Beispiel für eine statisch bereitgestellte Regel, dass der Name von Spring-RestControllern auch auf Controller enden muss:
In allen Projekten kann diese Regel dann einfach wie im unteren Code-Beispiel referenziert werden.
Für die Lang-Schicht bietet ArchUnit explizite Erweiterungspunkte, um die Definitionsmöglichkeiten für Architekturregeln zu erweitern. Ausgehend von dem Ansatz Architekturregeln mittels Prädikat und Bedingung zu formulieren
“classes/elements that ${PREDICATE} should ${CONDITION}”
stellt ArchUnit hierfür zwei Erweiterungspunkte zur Verfügung. Über eigene Implementierungen der Klassen DescribedPredicate<T>
und ArchCondition<T>
, wobei T das behandelte Java-Element (z. B. JavaClass) angibt, können spezifischere Unterstützungen zur Definition von Architekturregeln bereitgestellt werden.
Darüber hinaus ist auch eine Erweiterbarkeit der Core-Schicht möglich, um beispielsweise weitere Artefakte neben den Standard-Java-Elementen zu unterstützen. Hierzu muss jedoch auch der ClassFileImporter() erweitert werden, was ein deutlich tieferes Verständnis von ArchUnits Verarbeitung von Artefakten verlangt. Hier hilft es aus eigener Erfahrung sehr, dass ArchUnit ein OpenSource-Projekt und der Programmcode auf GitHub frei einsehbar ist.
Tipps aus dem Alltag für den Alltag
In der Praxis haben sich einige Handgriffe bewährt, die die Arbeit mit ArchUnit erleichtern und effizienter gestalten.
Logging – Mehr als tausend Worte
Tatsächlich ist ArchUnit erstmal sehr gesprächig und protokolliert nahezu jedes Java-Element, das analysiert wird. Während viele Entwicklungsumgebungen und Integrationsserver dies noch aushalten, steigt ein Maven Build über die Windows-Kommandozeile deutlich früher mal mit einem OutOfMemory Fehler aus.
Abhilfe schafft eine Logging-Konfiguration im Projekt, die das Logging entsprechend einschränkt. Typischerweise reicht eine Logback Konfiguration als logback.xml-Datei im Verzeichnis /src/test/resources mit folgendem Inhalt:
Fokus auf die eigene Software
Genauso wie Unit-Tests auf eine bestimmte „Einheit“ abzielen, so sollten auch die Architekturprüfungen auf den eigenen Code angewendet und nicht alle externen Abhängigkeiten überprüft werden. Hierzu stehen ArchUnit-Optionen bereit, die leicht in den eigenen Tests beim Import, oder im Falle der Kurzschreibweise als Optionen mitgegeben werden können:
Durch diese drei Optionen werden keine Testdateien analysiert. Externe JAR-Dateien und andere Archive sind auch ausgeschlossen.
Akzeptanz und Lerneffekte durch Begründungen
Während vor einigen Jahren noch Architekturvorgaben bei der Entwicklung einfach ohne Nachfragen befolgt wurden, so gehört heutzutage ein gutes Architekturverständnis und -interesse zum typischen Entwickler:innen-Skill-Set. Entsprechend wichtig ist auch die Nachvollziehbarkeit von Architekturregeln und deren Prüfungen, insbesondere wenn eine Verletzung gemeldet wird.
In der Testausgabe über den because-Aufruf ist ein kompakter Hinweis bei einer Regelverletzung deutlich besser zu lesen. Daher hat es sich in der Praxis bewährt, die ausgiebigere Begründung für eine Architekturregel beispielsweise auf einer Wiki-Seite im Intranet zu hinterlegen und einen Link dorthin anzubieten. Ein positiver Nebeneffekt hiervon ist, dass so auch gleich ein Verzeichnis aller definierten Architekturregeln entsteht, die in Abstimmungsterminen oder auch bei der Einführung für neue Entwickler:innen genutzt werden kann.
In der Praxis kann das dann beispielsweise wie in folgender because-Angabe aussehen:
.because("LocalDate statt java.util.Date verwenden - https://wiki.company.com/x/qQ6sB");
Freezing – Wenn man nicht auf der grünen Wiese startet
Wenn neue Architekturregeln für bestehende Anwendungen eingeführt werden, kann es vorkommen, dass mehr Verstöße identifiziert werden als direkt behoben werden können. In diesem Fall möchte man natürlich arbeitsfähig bleiben und primär verhindern, dass neue Verstöße hinzukommen. Hierfür bietet ArchUnit ein Feature namens Freezing. Es erlaubt, einen Test so zu kennzeichnen, dass bei seinem ersten Durchlauf alle Verstöße gemeldet und abgespeichert werden. Bei jedem weiteren Testdurchlauf werden dann nur neue Verstöße berichtet.
Das folgende Code-Beispiel zeigt, wie das Freezing-Feature für eine Prüfung einfach durch einen umschließenden freeze()-Methodenaufruf aktiviert werden kann.
Darüber hinaus muss noch eine Konfigurationsdatei archunit.properties mit folgendem Inhalt unter src/test/resources/ abgelegt werden:
Die gespeicherten Regelverletzungen werden in Dateien innerhalb des unter „path“ angegebenen Ordners abgelegt und können falls notwendig einfach gelöscht werden, um das Freezing zurückzusetzen.
Grenzen und Herausforderungen
Wie jedes Werkzeug hat auch ArchUnit Grenzen und Herausforderungen, mit denen man bewusst umgehen muss.
Im Bytecode liegt die Wahrheit – aber auch nicht mehr
Der technische Ansatz, Architekturregeln in Form von Unit-Tests zu prüfen, macht ArchUnit zu einem sehr komfortablen und leicht einzusetzenden Werkzeug. Gleichermaßen kommt es jedoch auch mit gewissen Limitierungen und ist auf statische, Bytecode-basierte Prüfungen beschränkt. Entsprechend sollte man sich bewusst sein, dass weder Artefakte, die nicht im Bytecode abgebildet sind, noch Laufzeit- oder Deployment-Informationen analysiert werden können.
Fokussierung – denn auch Automatisierung kostet
Genauso wie für alle Unit-Tests gilt auch für die ArchUnit-Tests, dass sie nicht kostenlos sind – weder ihre Implementierung noch ihre Ausführung. Durch die notwendige Code-Analyse wird die Ausführung von ArchUnit-Tests bei größeren Anwendungen auch gerne mal etwas teurer. Insofern empfiehlt es sich, nicht einfach alles zu testen, was möglich ist, sondern sich bewusst zu entscheiden, welche ArchUnit-Tests sinnvoll und insbesondere notwendig sind. Beispielsweise sind bei vielen Teams heutzutage sowieso Werkzeuge wie SonarQube, Checkstyle oder Findbugs zur Code-Analyse etabliert. Hier lassen sich insbesondere Code-Konventionen oft leichter und günstiger prüfen und besser in die gängigen Entwicklungsumgebungen integrieren.
Klarheit durch Zwang zur Eindeutigkeit
Will man Architekturregeln automatisiert prüfen, so erfordert die technische Umsetzung eindeutige Regel-Definitionen. Auch wenn sich diese Eindeutigkeit manchmal erst nach den ersten fehlgeschlagenen Tests einstellt, so sorgt spätestens dies für eine Klärung im Team oder mit dem verantwortlichen Architekten, wie die betroffene Regel genau definiert sein sollte.
Fazit und Cheat Sheet
Die automatisierte Prüfung von Architekturregeln entlastet genauso wie andere Code-Analysen die kostbaren manuellen Reviews. ArchUnit hat sich hierfür im Projekteinsatz als gute Ergänzung im Entwicklungswerkzeugkasten erwiesen. Natürlich gilt wie für jedes andere Werkzeug auch, dass ArchUnit bewusst gewählt werden sollte und für manche Code-Analysen auch passendere Werkzeuge existieren.
Eine der größten Herausforderung ist die Definition und Abstimmung der zu prüfenden Architekturregeln. Oft lohnt sich dieser Aufwand, da hier vermeintliche Abkürzungen in der Kommunikation frühzeitig transparent und geklärt werden. Nicht selten klären sich hierdurch Interpretationsspielräume, die sonst zu einer schleichenden Architekturerosion geführt hätten.
Letztendlich haben wir sehr positive Erfahrungen mit ArchUnit im Praxis-Einsatz gemacht und können nur empfehlen einen Blick darauf zu werfen. Aus diesem Grund haben wir auch ein ArchUnit Cheat Sheet zusammengestellt, das hier kostenlos heruntergeladen werden kann.
zurück zur Blogübersicht