ArchUnit

Lesezeit: 4 Minuten

ArchUnit ist eine Bibliothek für Java-Anwendungen, die Tests der Java-Anwendungsarchitektur ermöglicht. ArchUnit realisiert dies durch Bytecode-Analyse. ArchUnit-Assertions werden über eine Fluent API geschrieben.

Beziehungen zwischen Klassen

Folgende allgemeine Architektur-Vorgaben können beispielsweise leicht durch ArchUnit-Tests sichergestellt werden:

  • Alle Klassen in einem Package A dürfen nur von Klassen in einem Package B referenziert werden
  • Klassen mit einem Namensmuster A müssen in einem Package B liegen
  • Klassen eines Types A müssen ein Namensmuster B erfüllen
  • Klassen, die mit A annotiert sind, müssen in einem Package B liegen
  • Zwischen Klassen in einer Package-Struktur A darf es keine zyklischen Abhängigkeiten geben
// Beispiel: Repositories dürfen nur 
// in Domain Services referenziert werden
@ArchTest
public ArchRule repositoriesShouldOnlyBeAccessedByDomainServices = 
    classes().that()
    .haveNameMatching(".*Repository")
    .should().onlyBeAccessed()
    .byClassesThat()
    .resideInAPackage("..domain.service..");

Deklaration der Anwendungsarchitektur

ArchUnit erlaubt es auch die gesamte Package-Struktur einer Java-Anwendung zu deklarieren um darauf aufbauend Regeln anzuwenden. Aktuell stehen zwei Ansätze zur Verfügung. Eine layeredArchitecture ist gut geeignet zur Beschreibung eines klassischen Schichtenmodells.

layeredArchitecture()
    .layer("REST").definedBy("..rest..")
    .layer("Service").definedBy("..service..")
    .layer("Database").definedBy("..database..")

Darauf aufsetzend lassen sich dann die erlaubten Beziehungen zwischen den Packages deklarieren, in dem Beispiel sollte REST nur auf Service zugreifen dürfen und Service auf Database. Andere Beziehungen sind nicht erwünscht.

    .whereLayer("REST").mayNotBeAccessedByAnyLayer()
    .whereLayer("Service").mayOnlyBeAccessedByLayers("REST")
    .whereLayer("Database").mayOnlyBeAccessedByLayers("Service")

Daneben gibt es den Ansatz der onionArchitecture, auch bekannt als hexagonale Architektur oder als Entwurfsmuster Ports and Adapters. Der Gedanke hierbei ist es, dass eine Anwendung im Kern eine fachliche Domäne hat. Diese ist von einer Applikationsschicht umschlossen. An diese Schicht wiederum docken verschiedene Ports (eingehende Schnittstelle) und Adapters (ausgehende Schnittstelle) an. OnionArchitecture verwendet intern eine LayeredArchitecture. Anstelle einer Vielzahl an Layern werden hier Domain Models, Domain Services, Application Services und Adapters deklariert. Die erlaubten Beziehungen zwischen den verschiedenen Layern werden bereits durch den hexagonalen Ansatz vorgegeben und müssen hier nicht explizit spezifiert werden. Es wird z.B. implizit verifiziert, dass ein Adapter A nicht auf einen anderen Adapter B zugreift.

onionArchitecture()
    .domainModels("de.pflugradts.myapp.domain.model..")
    .domainServices("de.pflugradts.myapp.domain.service..")
    .applicationServices("de.pflugradts.myapp.application..")
    .adapter("REST", "de.pflugradts.myapp.connectors.rest..")
    .adapter("Message Queueing", "de.pflugradts.myapp.connectors.mq..")
    .adapter("Batch", "de.pflugradts.myapp.connectors.batch..");

Eigene Assertions

Darüber hinaus lassen sich eigene Assertions formulieren, die den Funktionsumfang von ArchUnit mit allem, was der Bytecode hergibt, erweitern können. Die gängige Syntax hierfür ist:

Classes that Predicate should Condition

Um ein eigenes Predicate oder eine Condition zu schreiben, wird eine generisch typisierte Klasse, ein DescribedPredicate<T> oder ein ArchCondition<T>, abgeleitet und bestimmte abstrakte Methoden implementiert. Hier kann dann auf den Typ T in Form eines Methodenparameters zugegriffen. In einem DescribedPredicate<JavaClass> steht in der zu implementierenden Methode apply(JavaClass) über die JavaClass die Klassenstruktur mit allen Methoden, Feldern, Konstruktoren etc. zum Auslesen zur Verfügung.

Anwendungsbeispiele aus der Praxis

  • ArchUnit kann verifizieren, dass bestimmte Methoden wie parallelStream aus der Java Streaming API in bestimmten Packages oder grundsätzlich nicht verwendet werden dürfen.
  • ArchUnit kann verifizieren, dass alle REST-Endpunkte durch bestimmte Annotationen abgesichert sind, damit kein unauthorisierter Zugriff erfolgen kann.
  • ArchUnit kann verifizieren, dass eine Mandantentrennung in einem Multimandantensystem bei jedem Datenbankzugriff eingehalten wird.
  • ArchUnit kann verifizieren, dass alle Klassen innerhalb einer Domäne eindeutig als z.B. AggregateRoot, Domain Entity, Domain Event, Value Object oder Repository deklariert sind.
  • ArchUnit kann verifizieren, dass alle Klassen in einem bestimmten Package einen dazugehörigen UnitTest haben, oder es etwa zu allen Repositories einen entsprechenden IntegrationTest gibt.
- Webseite von ArchUnit
- ArchUnit in PwMan3