© Martin Bergsma/Shutterstock.com
Architekturpatterns in Modulithen – Teil 3

Die Bude sauber halten


Puh, endlich geschafft. Die Artikelserie geht dem Ende zu. Diese Menge Code zu einem anständigen Modulithen zu formen, war ganz schön anstrengend. Zum Glück ist er jetzt fertig, alle Arbeit ist getan! Wie? Weiterentwicklung? Wartung und Betrieb? Neue Anforderungen? Das Team skalieren? Technische Schulden? Aber das Ding ist doch ganz neu! Warum müssen wir da schon wieder ran? Tja, machste nix. Oder doch?

Klar machen wir da was! Genau wie jede andere Software bleibt ein Modulith nur dann am Leben, wenn kontinuierlich weiter an ihm gearbeitet wird. Wenn man nicht ohnehin schon ständig neue Features und geänderte Anforderungen umsetzt, dann sind Sicherheitsupdates, abgekündigte Versionen, sich ändernde Abhängigkeiten, neue Tools und Erkenntnisse genug Gründe für nicht abreißende Weiterentwicklung. In einer großen, modulithischen Codebase gibt es dafür eine Menge Herausforderungen. Das können sein: großflächige Refactorings, häufiges Hinterfragen der System- und Anwendungsarchitektur und das Erreichen einer hohen Testabdeckung aller relevanten Aspekte, um dabei nichts kaputt zu machen. Wenn man das alles im Griff hat, dann spricht auch nichts dagegen, den Modulithen ganz Microservice-like mehrmals am Tag über die Continuous Delivery Pipeline auf Prod zu pushen.

Das Testuniversum

Je größer ein Modulith ist, desto mehr verschiedene Aspekte sind für die reibungslose Funktion entscheidend. Der Code in den Modulen muss tun was er soll, außerdem sollen die Module zusammengenommen das erwartete Verhalten an den Tag legen. Die Integration mit externen Abhängigkeiten und internen beweglichen Teilen wie Datenbanken, Caches und Events muss stabil laufen. Es gibt nichtfunktionale Anforderungen wie Performance, Resilience, Security. Fachliche Akzeptanzkriterien müssen eingehalten werden. Wie soll man all das testen und dabei nicht verrückt werden?

Es ist keine schlechte Idee, sich erst einmal an der klassischen Testpyramide [1] zu orientieren. Eine möglichst komplette Unit-Test-Abdeckung möchte man wahrscheinlich in den meisten Projekten haben. Aber braucht man Lasttests, wenn man nur 1000 Requests pro Tag beantwortet? Wie wichtig ist das automatisierte Sicherstellen von Performance, wenn kein echter User auf Antwort wartet? Welche Integrationstests braucht das Projekt unbedingt, welche sind vernachlässigbar? Muss man jeden fachlichen Edge Case abdecken oder reicht der Happy Path? Solche Fragen sollte man sich stellen und sich dann bewusst entscheiden, welchen Aspekt man in welcher Intensität automatisiert testen will. Dabei wird man den einen oder anderen Kompromiss zwischen kompletter Abdeckung aller Aspekte und dem dafür notwendigen Aufwand eingehen müssen.

Um ein einheitliches Bild in den Köpfen des Teams zu etablieren, eignet sich eine Visualisierung aller zu testenden Aspekte und der zugehörigen Testarten wie die Tabelle in Abbildung 1 zeigt.

franke_modulith_3_1.tif_fmt1.jpgAbb. 1: Die Testmatrix visualisiert, welcher Test was testet

Dort sieht man auf den ersten Blick anhand der grünen Haken, welche Art von Test in welcher Technologie für das Testen welches Aspekts zuständig ist. Die gelben Symbole visualisieren unvermeidbare Überschneidungen. So kann es beispielsweise passieren, dass durch eine fehlerhafte Modulintegration auch ein fachlicher Akzeptanztest auf die Bretter geht, weil er diese Integration durchläuft, obwohl er sie gar nicht explizit testen will. Die API-Security-Tests dagegen werden dadurch nicht behelligt.

In einem großen Modulithen hat so eine Matrix für gewöhnlich noch deutlich mehr Zeilen und Spalten als in Abbildung 1. Man kann so eine Definition des Testuniversums in einem Teamworkshop als Ist-Zustand und Soll-Zustand erarbeiten. Am Ende hat jeder ein einheitliches Bild im Kopf und weiß, wo die Reise hingeht. Testentwicklung läuft koordinierter ab, während das Team Sicherheit gewinnt, denn fehlende Tests gehen einem jetzt nicht mehr so schnell durch die Lappen, und man weiß genau, wo man hin greifen muss, wenn ein Test rot wird.

Wie wichtig ist die Testabdeckung?

Ergänzt wird das Konzept durch Meaningful Test Coverage [2]. Das bedeutet zunächst, dass man nicht nur sinnlose Tests schreibt, die einfach jede Zeile Code durchlaufen, um eine hohe Coverage zu erzielen. Jeder Test sollte einen sinnvollen Aspekt testen und entsprechend spezifische Assertions durchführen. Außerdem bedeutet es, dass man sich bewusst entscheidet, manchen Code nicht zu testen, wie z. B. generierten Code, Testcode, Konfigurationscode, und ihn dann auch aus der Coverage-Analyse auszuschließen. Auf fachlicher Ebene kann das bedeuten, dass man vielleicht nicht jeden fachlichen Edge Case automatisiert testen will, sondern nur die essenziellen Use Cases. Bei solchen Entscheidungen sollten die entsprechenden Stakeholder ein Wörtchen mitzureden haben. Am Ende steht das hehre Ziel, möglichst 100 Prozent der Dinge, die man tatsächlich testen möchte, durch sinnvolle Tests abzudecken. Das gilt für die Zeilen Code bei Unit-Tests, für Integrationspunkte bei Integrationstests, fachliche Aspekte bei Akzeptanztests und entsprechend auch für alle anderen Arten von Tests.

Isolation von Tests

Mit einem Test nur exakt den Aspekt anzusprechen, den man damit auch testen will, ist oft gar nicht so einfach. Jedes getestete Stück Code verwendet andere Klassen oder Abhängigkeiten, die man eigentlich nicht mittesten will. Wenn man für den Integrationstest einer Schnittstelle die ganze Anwendung hochfährt, braucht man auch alle Abhängigkeiten wie Datenbank, Filesystem, Messagingsysteme, die mit der Schnittstelle gar nichts zu tun haben. In der komplexen Codebase eines Modulithen ist das so viel, dass Laufzeit, Ressourcenverbrauch und Aussagekraft der Tests stark darunter leiden. Daher ist die Isolation des zu testenden Aspekts beim Testaufbau eine Kunst für sich. Im Modulithen trifft man auf unterschiedlichste Isolationsszenarien. Daher ist es sinnvoll, dafür einen Werkzeugkasten mit den richtigen Isolationsmethoden parat zu haben.

Auf Unit-Test-Ebene gibt es für Java eine Menge an Mocking Libraries wie Mockito [3] und Powermock [4], die es leicht machen, eine Code Unit zu isolieren, indem man Mocks für alle Abhängigkeiten konfiguriert. Im Spring Framework gibt es viele Möglichkeiten, für integrative Tests nur genau die Teile der Anwendung hochzufahren, die man b...

Neugierig geworden?

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang