12. Jun 2023
Lesedauer 12 Min.
Architektur-Check schneller als gedacht
ArchUnitNET
Wie Sie die Einhaltung von Architektur-Richtlinien mit automatisierten Unit-Tests prüfen.

Dass Unit-Tests zur Aufrechterhaltung der Qualität von Code geeignet sind, dürfte Lesern der dotnetpro bekannt sein. Unit-Tests kümmern sich indes im Allgemeinen lediglich darum, dass das „Codestück under Test“ die vorliegende Aufgabe beschreibungsgemäß korrekt erledigt.Ob die zu seiner Realisierung verwendete Architektur allerdings ebenfalls vernünftig ist, lässt sich bisher – zumindest nach landläufiger Meinung – nur durch manuelle Code Reviews verifizieren.Mit ArchUnitNET steht nun ein System zur Verfügung, das formalisierte Kriterien nicht mehr gegen die Ergebnisse von Test-Programmen, sondern gegen die Struktur der vorliegenden Assemblies zur Ausführung bringt.Während das Produkt im Java-Bereich zumindest bis zu einem gewissen Grad bekannt ist, ist es .NET-Nutzern eher weniger geläufig. Dies ist schon deshalb schade, weil das unter [1] bereitstehende Repositorium des Projekts – siehe hier auch Bild 1 – regelmäßige Aktualisierungen erfährt.

ArchUnitNETwird aktiv gepflegt(Bild 1)
Autor
Keine Unterstützung für .NET Standard
Beachten Sie vor der Entscheidung für xUnit, dass das Produkt – wie unter [6] im Detail beschrieben – keine Unterstützung für .NET Standard mitbringt.
Einrichtung der Infrastruktur
Obwohl ArchUnitNET im Prinzip auch im Stand-alone-Betrieb nutzbar ist, wird es normalerweise in ein Unit-Test-Framework eingebunden. Dabei unterstützen die Entwickler sowohl NUnit als auch xUnit. Da der Gutteil der von den Entwicklern zusammengestellten Beispiele und eigenen Testfälle auf xUnit basiert, wollen wir in den folgenden Schritten diese rezentere Variante verwenden.Wer stattdessen die „ältere“ NUnit-Variante nutzen möchte, kann die für NUnit notwendigen Adapter-Elemente nach folgendem Schema herunterladen und installieren:
PS> Install-<span class="hljs-keyword">Package</span> <span class="hljs-title">ArchUnitNET.NUnit </span>
Im Interesse der Bequemlichkeit wollen wir bei den folgenden Schritten auf Kommandozeilenebene arbeiten. Aktuelle Versionen von Visual Studio lassen dem Entwickler dabei die Wahl im Bereich des Terminal-Emulators: Der Autor wird bei dem nachfolgend beschriebenen Vorgehen mit der Developer PowerShell for VS2022 arbeiten, die als Teil seiner Visual-Studio-Installation verfügbar ist und auf die Versionsnummer 17.4.4 hört.Eine durch Eingeben des Befehls
dotnet <span class="hljs-comment">--version </span>
erfolgende Reflexion der .NET-Version meldet die Verfügbarkeit der Version 7.0.200-preview.22628.1 des Frameworks.Den Anfang machen wir, indem wir uns um die Erzeugung eines neuen Projektskeletts kümmern. Da für xUnit mittlerweile ein schlüsselfertiges Template zur Verfügung steht, lässt sich diese Aufgabe – nach dem Wechsel in das Verzeichnis, das den Code später aufnehmen soll – mit einem einzigen Kommando bewerkstelligen:
PS C:<span class="hljs-symbol">\U</span>sers<span class="hljs-symbol">\t</span>amha<span class="hljs-symbol">\s</span>ource<span class="hljs-symbol">\r</span>epos<span class="hljs-symbol">\t</span>amsarchtest>
dotnet new xunit
Im nächsten Schritt folgt das eigentliche Deployment der Bibliothek:
PS C:<span class="hljs-symbol">\U</span>sers<span class="hljs-symbol">\t</span>amha<span class="hljs-symbol">\s</span>ource<span class="hljs-symbol">\r</span>epos<span class="hljs-symbol">\t</span>amsarchtest>
Install-Package ArchUnitNET
Wer die PowerShell-Umgebung nur vergleichsweise selten verwendet, bekommt mitunter einen nach der Bauart Install-Package : Für die angegebenen Suchkriterien und den Paketnamen ”ArchUnitNET” wurde keine Übereinstimmung gefunden. aufgebauten Fehler angezeigt.In diesem Fall erfolgt das Deployment stattdessen durch die beiden folgenden Kommandos:
PS C:<span class="hljs-symbol">\U</span>sers<span class="hljs-symbol">\t</span>amha<span class="hljs-symbol">\s</span>ource<span class="hljs-symbol">\r</span>epos<span class="hljs-symbol">\t</span>amsarchtest>
dotnet add package TngTech.ArchUnitNET
PS C:<span class="hljs-symbol">\U</span>sers<span class="hljs-symbol">\t</span>amha<span class="hljs-symbol">\s</span>ource<span class="hljs-symbol">\r</span>epos<span class="hljs-symbol">\t</span>amsarchtest>
dotnet add package TngTech.ArchUnitNET.xUnit
Bei bestehender Internetverbindung sollten beide durchlaufen und die Projektstruktur modifizieren. Damit sind wir bereit für die Durchführung erster Unit-Tests.
Koppelungsmathematik erklärt
Haben Sie bisher noch nie Gedanken in das Thema der Softwarekoppelung investiert, so sollten Sie dies nachholen. Ted Faisons Klassiker „Event-Based Programming: Taking Events to the Limit“ [7] beschäftigt sich nicht nur mit eventorientierten Architekturen, sondern auch mit Koppelung – und ist nach Ansicht des Autors eines der wenigen Bücher, die seine Karriere nachhaltig verändert haben und die auf dem Schreibtisch keines Entwicklers fehlen sollten.
Erstmalige Inbetriebnahme von ArchUnitNET
An dieser Stelle können wir die Codedatei UnitTest1.cs öffnen, um das vom Codegenerator erzeugte Skelett um die folgenden Deklarationen zu erweitern:
<span class="hljs-keyword">using</span> ArchUnitNET.Domain;
<span class="hljs-keyword">using</span> ArchUnitNET.Loader;
<span class="hljs-keyword">using</span> ArchUnitNET.Fluent;
<span class="hljs-keyword">using</span> Xunit;
<span class="hljs-keyword">using</span> <span class="hljs-keyword">static</span> ArchUnitNET.Fluent.
ArchRuleDefinition;
Da wir in diesem Artikel keine Vollbesprechung von xUnit durchführen, sei nur darauf hingewiesen, dass das Framework zwei Arten von Testfall kennt. Ein „Fact“ ist ein gewöhnlicher Unit-Test, der – nach folgendem Schema – keine Parameter entgegennimmt:
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">UnitTest1</span>
{
[Fact]
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Test1</span>(<span class="hljs-params"></span>) </span>
<span class="hljs-function"> </span>{
}
}
„Theories“ hingegen beschreiben Unit-Tests, deren Verhalten von den angelieferten Parametern abhängig ist – so lässt sich beispielsweise das Korrektsein einer Methode überprüfen, die in bestimmten Betriebszuständen scheitern muss:
[<span class="hljs-name">Theory</span>]
[<span class="hljs-name">InlineData</span>(<span class="hljs-name">3</span>)]
[<span class="hljs-name">InlineData</span>(<span class="hljs-name">5</span>)]
public void MyFirstTheory(<span class="hljs-name">int</span> value)
{
Assert.True(<span class="hljs-name">IsOdd</span>(<span class="hljs-name">value</span>))<span class="hljs-comment">; </span>
<span class="hljs-comment">} </span>
<span class="hljs-comment">bool IsOdd(int value) </span>
<span class="hljs-comment">{ </span>
<span class="hljs-comment"> return value % 2 == 1; </span>
<span class="hljs-comment">} </span>
Um die Möglichkeiten und Grenzen des Systems zu veranschaulichen, wollen wir im ersten Schritt einen bekannten Problemfall realisieren. Bei der Nutzung von Konstanten verschiebt der .NET-Compiler die Deklaration zwischen den Nutzern, was zu Problemen bei der durch Reflexion erfolgenden Analyse der Architektur im Testfall erfolgt.Zur Illustration benötigen wir im ersten Schritt eine nach folgendem Schema aufgebaute Klasse, die ein statisch-konstantes Feld aufweist:
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> UnitTest1 ...
<span class="hljs-keyword">class</span> ClassWithStaticField
{
<span class="hljs-keyword">public</span> <span class="hljs-keyword">const</span> <span class="hljs-built_in">string</span> ConstField = <span class="hljs-string">"ConstField"</span>;
}
Im nächsten Schritt erzeugen wir eine weitere Klasse, die die in diesem statisch-konstanten Feld befindlichen Informationen aberntet. Beachten Sie, dass die Aberntung hier in einer als public void deklarierten Methode erfolgt:
<span class="hljs-keyword">class</span> <span class="hljs-title">ClassAccessingField</span>
{
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">MethodAccessingConstField</span>(<span class="hljs-params"></span>) </span>
<span class="hljs-function"> </span>{
<span class="hljs-keyword">var</span> a = ClassWithStaticField.ConstField;
}
}
In der Theorie würden wir davon ausgehen, dass die Klasse ClassAccessingField nun eine Typ-Abhängigkeit zur Klasse ClassWithStaticField aufweist.Zur Überprüfung unserer Annahme erzeugen wir im ersten Schritt ein Architektur-Objekt, das auf die Klasse hinweist. Durch die Methode method.GetTypeDependencies().ToList() generieren wir dann eine Liste, die alle zu dieser Klasse gehörenden Typ-Abhängigkeiten enthält:
[<span class="hljs-name">Fact</span>]
public void Test1()
{
var method = Architecture.GetClassOfType(<span class="hljs-name">typeof</span>(
ClassAccessingField)).GetMethodMembers()
.First(<span class="hljs-name"><span class="hljs-builtin-name">member</span></span> => member.FullNameContains(
<span class="hljs-string">"MethodAccessingConstField"</span>))<span class="hljs-comment">; </span>
<span class="hljs-comment"> var methodTypeDependencies = </span>
<span class="hljs-comment"> method.GetTypeDependencies().ToList(); </span>
In diesem ersten Beispiel wollen wir den xUnit-Testrunner noch nicht aktivieren. Aus diesem Grund überprüfen wir den Rückgabewert unter Nutzung eines Assert-Makros, was zu folgendem Code führt:
<span class="hljs-type">Assert</span>.<span class="hljs-type">Contains</span>(<span class="hljs-type">Architecture</span>.<span class="hljs-type">GetClassOfType</span>(typeof(
<span class="hljs-type">ClassWithStaticField</span>)), methodTypeDependencies);
}
An dieser Stelle können Sie die Datei speichern und in der Developer PowerShell durch folgendes Kommando zur Ausführung bringen:
PS C:<span class="hljs-symbol">\U</span>sers<span class="hljs-symbol">\t</span>amha<span class="hljs-symbol">\s</span>ource<span class="hljs-symbol">\r</span>epos<span class="hljs-symbol">\t</span>amsarchtest> dotnet test
Lohn der Mühen ist das in Bild 2 gezeigte Ergebnis. Das Aufscheinen der roten Strings informiert darüber, dass das Assert fehlgeschlagen ist und die Ausführung des Architektur-Tests deshalb nicht erfolgreich verlief.

Die Auflösung der Abhängigkeitenscheitert, wenn ein statisches Feld involviert ist(Bild 2)
Autor
Mehr zu PlantUML
Möchten Sie mehr über PlantUML erfahren möchten, so empfiehlt sich die unter [8] bereitstehende Webseite des Projekts. Beachten Sie, dass sich PlantUML außerdem verschiedene integrierte Entwicklungsumgebungen einbinden lässt – Wikipedia pflegt unter [9] eine Liste diverser hilfreicher Plug-ins.
Exkurs: Nutzung des xUnit-Testrunners für ArchUnit-Testfälle
Die in Bild 2 gezeigte rote Ausgabe mag genau über die Fehlschläge im Bereich der Ausführung informieren. Bequemer wäre es allerdings, wenn die Ergebnisse in einer visuell leichter zu erfassenden Art und Weise dargestellt werden würden.Dieses Ziel lässt sich dadurch erreichen, dass wir die Ausführung unserer Testfälle fortan unter Nutzung des vom xUnit-Framework zur Verfügung gestellten Testrunners bewerkstelligen. Im ersten Schritt müssen wir dazu nach folgendem Schema einige zusätzliche Klassen hinzufügen, um unseren Tests mehr Fleisch zur Verfügung zu stellen:
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ExampleClass</span> </span>
{ }
<span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">ForbiddenClass</span> </span>
{ }
Weil wir in den folgenden Schritten für die Aktivierung der einzelnen Testfälle auf nach dem Schema arch.check(rule) aufgebaute Statements setzen wollen, ist außerdem ein zusätzliches Using erforderlich. Unterbleibt dies, so scheitert die Kompilation der Tests mit irreführenden Fehlermeldungen:
using ArchUnitNET.xUnit<span class="hljs-comment">; </span>
Im nächsten Schritt ist ein Architekturobjekt erforderlich. Dabei handelt es sich um eine Art Containerklasse, die alle zu einem Testprozess gehörenden Assemblies und/oder Elemente zusammenfasst. Das Architektur-Objekt dient ArchUnitNET als Datenbasis für die Überprüfung von Regeln – nur jene Klassen und Elemente werden von der Regel beachtet, die in der jeweiligen Architektur enthalten sind.Für unser erstes kleines Beispiel wollen wir nach folgendem Schema deklarieren:
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">UnitTest1</span> {
<span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">readonly</span> Architecture myA =
<span class="hljs-keyword">new</span> ArchLoader().LoadAssemblies(<span class="hljs-keyword">typeof</span>(
ClassWithStaticField).Assembly,
<span class="hljs-keyword">typeof</span>(ForbiddenClass).Assembly).Build();
}
LoadAssemblies nimmt bei Bedarf mehrere Parameter entgegen. Die einzelnen Parameter entstehen dabei normalerweise nach dem Schema typeof(ForbiddenClass).Assembly. Sie können so aus einer einzelnen Klasse einen Verweis erzeugen, der gleich die gesamte Assembly greift.Die Nutzung der Klassen ClassWithStaticField und ForbiddenClass erfolgt hier primär aus didaktischen Gründen, da beide in derselben Assembly liegen – in der Praxis ist es möglich, den Reflexionsprozess auf mehrere Assemblies auszuweiten und so komplexere Systemarchitekturen automatisiert auf Korrektheit zu überprüfen.Im vorliegenden Code findet sich eine weitere Besonderheit: Als Variablenname entscheidet sich der Autor hier für myA und nicht für das in Beispielcode sonst gerne verwendete Architecture. Sinn dieser Devianz ist, dass ohne den Testrunner geschriebene Testfälle ebenfalls auf das Architecture-Objekt zurückgreifen; dabei meinen sie aber das „globale“ Einstiegs-Objekt von ArchUnitNET. Würden wir denselben Namen verwenden, käme es an dieser Stelle zu Kollisionen.Zu guter Letzt sei darauf hingewiesen, dass die Architecture-Klasse im Rahmen des Bring-ups durchaus aufwendige Reflexionsprozesse durchführt. Es ist im Interesse des höchstmöglichen Durchsatzes empfehlenswert, die Klasseninstanzen so gut wie möglich zu rezyklieren.Wir können unseren bisher erzeugten Testfall nun nach folgendem Schema neu anschreiben:
[<span class="hljs-name">Fact</span>]
public void Test1()
{
IArchRule rule = Classes().That().HaveNameContaining(
<span class="hljs-string">"ClassAccessingField"</span>).Should()
.NotDependOnAny(<span class="hljs-name">Classes</span>().That().HaveNameContaining(
<span class="hljs-string">"ClassWithStaticField"</span>))<span class="hljs-comment">; </span>
<span class="hljs-comment"> rule.Check(myA); </span>
<span class="hljs-comment">} </span>
In xUnit ausführbare Testfälle entstehen normalerweise als Instanzen des Hilfsinterfaces IArchRule. In ihm wird danach unter Nutzung der xUnit-eigenen Beschreibungssprache ein mehr oder weniger komplizierter Regelsatz angelegt, der die eigentliche durchzuführende Überprüfung beschreibt. Die Nutzung der Methode HaveNameContaining ermöglicht uns dabei das Reduzieren der von Classes().That() zurückgelieferten Allklasse; die Methode Should erlaubt dann im Zusammenspiel mit NotDependOnAny das Anwenden des eigentlichen, maschinell zu überprüfenden Constraints.Eine in Form einer IArchRule-Instanz vorliegende Regel unterscheidet sich von den weiter oben angeschriebenen impliziten Tests insofern, als sie von Haus aus im Raum steht – die Methode classes legt ja nicht fest, welche Assemblies und/oder Namespaces in die Basismenge einzubeziehen sind, die am Ende für die Berechnung eines eventuellen Regelverstoßes herangezogen werden wird.Zur Lösung dieses Problems müssen wir auf die weiter oben besprochene Architecture-Klasse zurückgreifen, die nach Inklusion des Moduls ArchUnitNET.xUnit die check-Methode zur Verfügung stellt.An dieser Stelle sind wir abermals für eine Programmausführung durch Eingabe von dotnet test bereit – Bild 3 zeigt, dass uns der Testrunner über das erfolgreiche Durchlaufen aller Architekturtests informiert. Eine logische Reflexion der in der Rule angelegten Testfälle bestätigt dann, dass es sich dabei abermals um den von weiter oben bekannten Fehler handelt.

Der grüne Text besagt:Hier ist alles in Ordnung!(Bild 3)
Autor
Einen „Fehlschlag des Testfalls“ können wir dann insofern provozieren, als wir die – übrigens unter [2] im Detail erklärte – Besonderheit im Zusammenspiel mit statischen Klassen nicht mehr berücksichtigen:
[<span class="hljs-name">Fact</span>]
public void Test1()
{
IArchRule rule = Classes().That().HaveNameContaining(
<span class="hljs-string">"ClassAccessingField"</span>).Should()
.DependOnAny(<span class="hljs-name">Classes</span>().That().HaveNameContaining(
<span class="hljs-string">"ClassWithStaticField"</span>))<span class="hljs-comment">; </span>
<span class="hljs-comment"> rule.Check(myA); </span>
<span class="hljs-comment">} </span>
Wenn Sie diese Version der .cs-Datei speichern und wieder durch Eingabe von dotnet test zur Ausführung bringen, sehen Sie einen roten Text, der auf einen Fehler hinweist (Bild 4).

Der xUnit-Runner informiertbei Bedarf ebenfalls über Fehler(Bild 4)
Autor
Falls Sie in Ihrer Applikation unbedingt ohne das Einbinden des Pakets auskommen möchten, bedeutet dies nicht, dass Sie auf den Komfort des „gezielten“ Ausführens von Objekten gegen Architektur-Container verzichten müssen. Der in diesem Fall anzuwendende Code basiert abermals auf Assert und nutzt eine in den individuellen Regeln angelegte Hilfsmethode mit dem allgemein gleichen Verhalten:
[<span class="hljs-name">Fact</span>]
public void Cycles()
{
IArchRule rule = Slices().Matching(
<span class="hljs-string">"Module.(*)"</span>).Should().BeFreeOfCycles()<span class="hljs-comment">; </span>
<span class="hljs-comment"> Assert.False(rule.HasNoViolations(Architecture)); </span>
<span class="hljs-comment"> //rule.Check(Architecture); </span>
<span class="hljs-comment">} </span>
Angemerkt sei in diesem Zusammenhang noch, dass die Ausführung von Testfällen unter Nutzung des Testrunners in Tests des Autors mit wesentlich höherem Rechenleistungsverbrauch einhergeht.Wohl ob der im Hintergrund stattfindenden umfangreichen Reflexionen und der durch Strukturen wie Classes.That ausgelösten Reduzierungen ist auf dem (gut ausgestatteten) MSI GF65 eine Wartezeit spürbar – würde man die Tests „freihändig“ ausführen, so erfolgte die Ausführung des Befehls dotnet test instantan.
Fortgeschrittene Analyse der Struktur von Applikationen
Unsere Experimente mit statischen Feldern demonstrieren die Probleme, die im Zusammenhang mit diesen Elementen auftreten. In der Praxis dürfte ArchUnitNET allerdings vor allem zur Verifikation der Korrektheit von Programmarchitekturen zum Einsatz kommen. Aus diesem Grund wollen wir im nächsten Schritt einige Beispiele durchsprechen, um die Überprüfungssyntax en vivant kennenzulernen.Im ersten Akt wollen wir hierbei eine zusätzliche Abstraktionsebene einziehen. IObjectProvider-Instanzen repräsentieren eine eingeschränkte Menge von Elementen, die den diversen von ArchUnitNET implementierten Mengen-Operationen unterwerfen werden können und so das kompaktere Formulieren der Testregeln erlauben. Die einfachste Variante betrifft Klassen und ist nach folgendem Schema aufgebaut:
<span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IObjectProvider<<span class="hljs-keyword">Class</span>> ExampleClasses =
Classes().That().ImplementInterface(
<span class="hljs-string">"IExampleInterface“).As(„Example Classes"</span>);
Das Partikel Classes().That() bietet diverse Filtermethoden an, die durch Reflexion in Visual Studio sichtbar gemacht werden können. Die hier gezeigte ImplementInterface ergreift all jene Klassen, die das als Parameter genannte Interface realisieren.Da moderne Softwarearchitekturen nicht nur Klassen, sondern auch Typen, Interfaces und andere Elemente erweitern, ist die IObjectProvider-Basisklasse als Generic aufgebaut. Für die Verarbeitung vom Typen oder Interfaces vorgesehene IObjectProvider-Instanzen lassen sich nach folgendem Schema erzeugen – auch hier gilt, dass die Regeln mit verschiedenen Einschränkungen ausgestattet werden dürfen:
<span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IObjectProvider<IType> ForbiddenLayer =
Types().That().ResideInNamespace(
<span class="hljs-string">"ForbiddenNamespace"</span>).<span class="hljs-keyword">As</span>(<span class="hljs-string">"Forbidden Layer"</span>);
<span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> IObjectProvider<<span class="hljs-keyword">Interface</span>>
ForbiddenInterfaces =
Interfaces().That().HaveFullNameContaining(
<span class="hljs-string">"forbidden"</span>).<span class="hljs-keyword">As</span>(<span class="hljs-string">"Forbidden Interfaces"</span>);
Mit den „angelegten Primitiva“ lässt sich wie gewohnt makeln. Ein interessantes Beispiel ist die folgende Deklaration, die die als „verboten“ bezeichneten Typen auf den Aufenthaltsraum in dem Forbidden-Layer beschränkt:
IArchRule forbiddenInterfacesShouldBeInForbiddenLayer =
Interfaces().That().Are(ForbiddenInterfaces).<span class="hljs-keyword">Should().</span>
<span class="hljs-keyword"> </span> <span class="hljs-keyword">Be(ForbiddenLayer); </span>
...
forbiddenInterfacesShouldBeInForbiddenLayer.Check(
Architecture)<span class="hljs-comment">; </span>
Sofern das an Check übergebene Architektur-Objekt irrlaufende Elemente aufweisen würde, würde die Ausführung dieses Testfalls scheitern – Entwickler werden so also dazu gezwungen, „neu angelegte“ Interfaces im korrekten Namespace zu halten – zumindest dann, wenn alle Namen im Architektur-Testfall angelegt sind.Außerdem ist es zur Kompakthaltung von Testfällen möglich, nach folgendem Schema mehrere Regeln in einem Rule-Objekt zusammenzufassen:
IArchRule combinedArchRule =
exampleClassesShouldBeInExampleLayer.<span class="hljs-keyword">And</span>(
forbiddenInterfacesShouldBeInForbiddenLayer);
combinedArchRule.<span class="hljs-keyword">Check</span>(Architecture);
Lohn der Mühen ist, dass die hier zusammengefassten „mehreren“ Regeln mit einem einzelnen check-Aufruf gegen eine Architektur zum Einsatz gebracht werden – dies führt zu einer Reduktion des Code-Umfangs der Test-Suite und erhöht ihre Wartbarkeit.Wichtig ist in diesem Zusammenhang außerdem, dass ArchUnitNET auch die korrekte Benennung der Klassen überprüfen kann. Ein Beispiel ist der Befehl HaveNameContaining, der überprüft, ob die Namen aller in der Menge zusammengefassten Klassen den passenden String aufweisen:
<span class="hljs-selector-tag">Classes</span>()<span class="hljs-selector-class">.That</span>()<span class="hljs-selector-class">.AreAssignableTo</span>(ForbiddenInterfaces).
<span class="hljs-selector-tag">Should</span>()<span class="hljs-selector-class">.HaveNameContaining</span>(<span class="hljs-string">"forbidden"</span>)
Auch hier gilt naturgemäß, dass das korrekte Funktionieren des Unit-Tests immer die Pflege von ForbiddenInterfaces voraussetzt – fügt ein Entwickler eine Klasse hinzu, ohne dass diese in ForbiddenInterfaces aufscheint, so kann er die Architektur-Konsistenz auf diese Art und Weise zerstören.Eine weitere Überprüfung betrifft „schwarze Aufrufe“, die den architekturalen Regeln zuwiderlaufen. Sie lassen sich durch Nutzung des Makros NotCallAny eliminieren:
<span class="hljs-selector-tag">Classes</span>()<span class="hljs-selector-class">.That</span>()<span class="hljs-selector-class">.Are</span>(<span class="hljs-selector-tag">ExampleClasses</span>)<span class="hljs-selector-class">.Should</span>()<span class="hljs-selector-class">.NotCallAny</span>(
<span class="hljs-selector-tag">MethodMembers</span>()<span class="hljs-selector-class">.That</span>()<span class="hljs-selector-class">.AreDeclaredIn</span>(<span class="hljs-selector-tag">ForbiddenLayer</span>).
<span class="hljs-selector-tag">Or</span>()<span class="hljs-selector-class">.HaveNameContaining</span>("<span class="hljs-selector-tag">forbidden</span>"))
Fortgeschrittene Verifikation um das MVC-Design-Pattern
Microsofts Entscheidung, in Windows Phone 7 statt auf MVC auf MVVM zu setzen, wurde nicht durchgängig mit Begeisterung aufgenommen. Unterm Strich gilt allerdings nach wie vor, dass die Einhaltung eines derartigen Architekturdesigns besser ist als die Einhaltung von gar keinem.Da insbesondere wenig diensterfahrene Entwickler oft daran scheitern, den Sinn der als Belästigung empfundenen Einschränkung zu verstehen, erlaubt ArchUnitNET auch dessen programmiertechnische Überprüfung.Als Paradebeispiel präsentiert das Entwicklerteam dabei das Erzwingen einer Separation zwischen View und Controller (siehe Bild 5). Zum Sicherstellen reicht es aus, nach folgendem Schema eine Regel gegen die zu verifizierende Codebasis anzuwenden:
Nicht jeder Zugriffist legal(Bild 5)
Bild: ArchUnitNET
IArchRule rule = <span class="hljs-keyword">Types</span>().That().ResideInNamespace(
<span class="hljs-string">"Model"</span>).Should() .NotDependOnAny(<span class="hljs-keyword">Types</span>().That().
ResideInNamespace(<span class="hljs-string">"Controller"</span>));
Ein weiteres häufiges Ärgernis in größeren Projekten ist die Missachtung von Namenskonventionen. Nur allzu gern wird beispielsweise festgelegt, dass alle vom Interface X abzuleitenden Klassen irgendwie auch den Namen des Interfaces als Klassennamen mitbringen müssen (siehe Bild 6).

ArchUnitNETnimmt sich die nur allzu beliebten Benennungsstreitigkeiten bei Bedarf bereitwillig zur Brust(Bild 6)
Autor
Wie im Fall von MVC gilt auch hier, dass eine nach dem folgenden Schema aufgebaute Regel jegliche weitere Diskussion über Benennungsrichtlinien obsolet macht:
<span class="hljs-attribute">IArchRule rule</span> = Classes().That().AreAssignableTo(
typeof(ICar)).Should().HaveNameContaining(<span class="hljs-string">"Car"</span>);
Fortgeschrittener Vergleich gegen UML-Diagramme
Während unserer Analyse von „fortgeschrittenen“ architekturalen Testfällen haben wir immer wieder festgestellt, dass erfolgreiche Unit-Tests unter Nutzung von ArchUnitNET die Mithilfe der Entwickler voraussetzen – wenn der Architekt ihnen nicht permanent „hinterherprogrammiert“, so können die Testfälle über kurz oder lang die Architektur nicht mehr erfolgreich überprüfen.Schöner wäre es, wenn das manuelle Aktualisieren der Instanzen nicht mehr nötig wäre, sprich das Unit-Testing-Framework sich die Architekturdaten also aus einer anderen, architektenfreundlicheren Darstellungsweise beschaffen könnte.Die Integration in PlantUML ermöglicht dies in vielerlei Hinsicht. PlantUML ist ein im Jahr 2009 erstmals spezifiziertes Beschreibungsformat, das eine fast menschenlesbare Programmiersprache zur Beschreibung von UML-Beziehungen heranzieht. Im Interesse der Kompaktheit können wir PlantUML an dieser Stelle nicht zur Gänze vorstellen, weshalb wir uns stattdessen am unter [3] bereitstehenden und vom Entwicklerteam zur Verfügung gestellten Beispiel orientieren wollen.Zur nähergehenden Analyse wollen wir uns im ersten Schritt das unter [4] bereitstehende PUML-File ansehen. Seine erste Aktion ist die Deklaration der einzelnen Pakete, was nach folgendem Schema erfolgt:
<span class="hljs-selector-attr">[Addresses]</span> <<<span class="hljs-selector-tag">ExampleTest</span><span class="hljs-selector-class">.PlantUml</span><span class="hljs-selector-class">.Addresses</span>.*>>
<span class="hljs-selector-attr">[Orders]</span> <<<span class="hljs-selector-tag">ExampleTest</span><span class="hljs-selector-class">.PlantUml</span><span class="hljs-selector-class">.Orders</span>.*>>
<span class="hljs-selector-attr">[Products]</span> <<<span class="hljs-selector-tag">ExampleTest</span><span class="hljs-selector-class">.PlantUml</span><span class="hljs-selector-class">.Product</span>.*>>
...
Den zweiten Teil bildet die nach folgendem Schema aufgebaute Deklaration der Beziehung zwischen den einzelnen Domänen:
<span class="hljs-string">[Orders]</span> --> <span class="hljs-string">[Products]</span>
<span class="hljs-string">[Orders]</span> --> <span class="hljs-string">[Addresses]</span>
...
Unter [5] findet sich übrigens ein durchaus leistungsfähiges (wenn auch werbeverseuchtes) Analysesystem, das – wie in Bild 7 gezeigt – PUML-Dateien auf Zuruf rendert.

Als Referenz:der Inhalt der PUML-Datei(Bild 7)
Autor
Die eigentliche Verifikation des Konformgehens zwischen der vorliegenden Architektur und den in der PUML-Datei angelegten Elementen erfolgt dann durch einen einzelnen Aufruf der Methode AdhereToPlantUmlDiagram:
[Fact]
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> ClassesShouldAdhereToShoppingExampleConside</span>
<span class="hljs-function"> <span class="hljs-title">ringOnlyDependenciesInDiagram</span>(<span class="hljs-params"></span>) </span>
{
<span class="hljs-keyword">var</span> filename = <span class="hljs-string">"./Resources/shopping_example.puml"</span>;
IArchRule adhereToPlantUmlDiagram =
Types().Should().AdhereToPlantUmlDiagram(filename);
adhereToPlantUmlDiagram.Check(Architecture);
}
Fazit
Die Nutzung von ArchUnitNET ermöglicht Entwicklern das schnelle Sicherstellen der Einhaltung von Architektur-Guidelines im Rahmen von automatisierten Unit-Tests. Die insbesondere bei architekturalen Konflikten schnell unschön werdenden Code Reviews werden bei automatisierter Ausführung der Unit-Tests entschärft, und sie können wieder ihrer ursprünglichen Aufgabe der Steigerung der Codequalität nachgehen.Unterm Strich ist ArchUnitNET ein Werkzeug, das man mit Sicherheit nicht jeden Tag braucht – braucht man es aber und kennt es nicht, so gehen mitunter Tausende von Mannstunden verloren.Fussnoten
- ArchUnitNET auf GitHub, https://github.com/TNG/ArchUnitNET
- ArchUnitNET, Constant Fields, http://www.dotnetpro.de/SL2307ArchUnit1
- ArchUnitNET, ExampleTest, http://www.dotnetpro.de/SL2307ArchUnit2
- ArchUnitNET, shopping_example.puml, http://www.dotnetpro.de/SL2307ArchUnit3
- PlantUML, http://www.plantuml.com/plantuml/uml/
- xUnit-Dokumentation, Why doesn't xUnit.net support netstandard?, https://xunit.net/docs/why-no-netstandard
- Ted Faison, Event-Based Programming: Taking Events to the Limit, Apress, 2011, ISBN 978-1-4302-4326-7,
- PlantUML – Ein kurzer Überblick, https://plantuml.com/de/
- PlantUML auf Wikipedia, https://de.wikipedia.org/wiki/PlantUML