Softwareentwicklung

Jigsaw: Testen im Java-Modulsystem

: Softwareentwickler Christian Stein hat sich mit dem Thema Testing in Jigsaw auseinandergesetzt. Hier gibt er einen Einblick in die Funktionsweise.

In diesem Blogbeitrag geht es um die Organisation, das Auffinden und das Ausführen von Tests im Modulsystem von Java – auch genannt Jigsaw. Es ist keine Einführung in das Java Modulsystem. Und wer gleich den Code sehen will, wird hier fündig.

Zunächst ein kleiner Exkurs in die Vergangenheit, denn schon damals stand diese Frage im Raum: “Wo soll ich Testklassen ablegen?”

  • Lege Testklassen im selben Verzeichnis ab, in dem sich auch die Produktionsklassen befinden.

Zum Beispiel:

Für kleinere Projekte war das Vorgehen okay – wobei schon damals viele Entwickler bei größeren Projekten die Vermischung von Produktions- und Testklassen als Nachteil dieses Ansatzes empfanden. Denn der Aufwand für das finale Zusammenstellen des Produktes wurde mit der Zeit immer höher, weil die Testklassen dabei rausgefiltert werden mussten.

  • Deswegen war es einfacher, die Testklassen in einem separaten aber analog aufgebauten Verzeichnisbaum abzulegen.

Dieser Ansatz ermöglicht es, dass alle Tests weiterhin auf die gleichen Elemente der Produktionsklassen zugreifen können.

Auf dem Klassenpfad

Wodurch war das damals und ist es heute weiterhin möglich? Durch den Klassenpfad!

Jedes Element des Klassenpfads wird der Laufzeitumgebung als Ursprung eines Ressourcenbaums zur Verfügung gestellt. Ein spezieller Ressourcentypus sind Javaklassen (.class-Dateien), die wiederum zu einem Paket (package) gehören. Der Klassenpfad schreibt dabei nicht vor, wie häufig ein Paket deklariert werden darf. Dadurch werden alle Ressourcen zu einem logischen Baum zusammengefügt, was zu einer ähnlichen Situation führt, als ob man wie früher alle Ressourcen unter einem physikalischen Verzeichnis ablegte. Testklassen können somit so auf die Produktionsklassen zugreifen, als ob sie im selben Verzeichnis lägen: Java packages werden wie “white boxes” behandelt. Das schließt den Zugriff auf Klassenelemente, die mit package private oder mit protected versehen sind, ein.

Schonmal eine Testklasse in einem anderen Paket als die zu testende Produktionsklassen abgelegt?

Das ist dann das “Black-Box”-Testen! Hier gelten alle Zugriffsregeln, die es für Sichtbarkeiten von Typen in andere Paketen gibt. Wie lauten diese Regeln? Hinweis: weiter unten folgt eine Übersicht zum diesem Thema.

Auf zu neuen Ufern: Hallo Jigsaw, hallo Java Module!

Mit dem Java-Modulsystem kann man eine Gruppe von Paketen unter einem Modulnamen zusammenfassen. Dabei kann man als Autor eines Moduls frei entscheiden, welche der Pakete für andere Module zur Verfügung stehen. Wenn man nun die oben beschriebene Idee der separierten Verzeichnisse einfach auf das Modulsystem überträgt, entsteht das folgende Bild:

Die linke Spalte main und die rechte Spalte test/black.box enthalten keine großen Überraschungen. Anders die mittlere Spalte test/com.xyz oder die Spalte white box; hier wurde nämlich eine Datei module-info.[java|test] hinzugefügt. Bevor wir aber das Thema White-Box-Testen vertiefen, starten wir mit den beiden einfacheren Modulen.

Module com.xyz

  • Das Module namens com.xyz enthält ein paar ausgedachte Einträge.
  • Es enthält die Pakete com.abc und com.xyz.
  • Es exportiert einzig und allein das Paket com.xyz.
Hinweis: Das Paket com.abc sollte nicht in einem Modul namens com.xyz auftauchen. Warum nicht? Stephen erläutert in seinem Blog JPMS module naming die Details.

Module black.box

  • Das Testmodul black.box benötigt das Modul com.xyz sowie eine Reihe anderer Module rund ums Testen.
  • Es kann dabei nur auf zugreifbare Typen in diesen anderen Modulen zugreifen (nämlich solche, die mit public versehen sind und sich gleichzeitig in einem exportierten Paket befinden).
  • Das gilt natürlich ebenso für unser com.xyz-Modul: Tests können auf öffentliche Klassen im Paket com.xyz zugreifen – nicht aber auf Klassen im geschützten Paket com.abc, selbst wenn diese public sind.
  • Zusätzlich erlaubt das black.box-Modul mittels open tiefe Reflexion, damit Testframeworks auch Package-Private-Tests auffinden und ausführen können.

Black-Box-Testen ist allerdings der einfache Teil der Geschichte. Das black.box-Testmodul ist quasi der erste Kunde des Hauptmoduls com.xyz. Das Testmodul hält sich an die vom Modulsystem vorgegebenen Grenzen – so wie jedes andere Modul auch.

Es folgt der spannende Teil.

Modular White Box Testing

Zunächst erweitern wir die Zugriffstabelle um eine Spalte. Nämlich um eine Spalte, welche die Zugriffsmöglichkeiten aus einem fremden Modul beschreibt.

Zugriffstabelle

Die Klasse A in package foo enthält jeweils ein Feld für jeden Zugriffsmodifikator. Jede Spalte von B bis F steht für eine andere Klasse und zeigt die Sichtbarkeit an: Ein ✅ bedeutet, dass das entsprechende Feld von A sichtbar ist; ein ❌ steht für “nicht sichtbar”.

  • B – gleiches module, gleiches package, andere Datei: package foo; class B {}
  • C – gleiches module, anderes package, Ableitung: package bar; class C extends foo.A {}
  • D – gleiches module, anderes package, beziehungslos: package bar; class D {}
  • E – anderes module, package foo wird exportiert: package bar; class E {}
  • F – anderes module, package foo wird nicht exportiert package bar; class F {}

Die Spalten E und F wurden bereits im obigen Abschnitt “Modulares Black-Box-Testen” behandelt. Wobei F nur zeigt, dass selbst mit public modifizierte Typen aus nicht-exportierten packages eben nicht sichtbar sind. Aber wir möchten ja Unittests so schreiben wie immer, und dabei auch auf interne Typen zugreifen können.

Wir wollen also B, C und D zurück!

Damit wir das gewohnte Verhalten wieder herstellen, können wir entweder das komplette Java-Modulsystem (für’s Testen) ausschalten. Oder wir nutzen einen neuen Weg der es ermöglicht, dass sich Test- und Haupttypen logisch in ein und demselben Modul befinden. Analog zu damals, als die Lösung split packages waren, die vom class-path aufgelöst wurden. Same same but different. Nur, dass split packages in der modularen Welt von Jigsaw nicht mehr erlaubt sind.

Es gibt mindestens drei Möglichkeiten, wie man die strikten Grenzen des Java-Modulsystems beim Testen umgehen kann.

Zurück zum classpath

Alle module-info.java-Dateien löschen, oder diese zumindest vom Kompilieren ausschließen – und schon ignorieren die Tests die Grenzen des Modulsystems! Dadurch werden, neben internen Details von Java selbst, auch Interna von anderen und eben der eigenen Bibliothek verfügbar. Letzteres war das Ziel – doch die Kosten, es zu erreichen, sind hoch.

Wie aber können wir die Grenzen des Modulsystems intakt lassen und trotzdem die internen Typen der eigenen Bibliothek testen? Dazu mehr in den nächsten zwei Abschnitten.

Modular White Box Testing mit module-info.java in src/test/java

Die für den Testautor einfachste Variante besteht darin, eine Beschreibung für ein Testmodul anzulegen. Die Beschreibung kann mit der gleichen Syntax geschehen, die bei normalen Modulen eingesetzt wird:

So kann ein Testmodul aussehen

Dabei wird es in zwei logische Abschnitte geteilt:

  1. Kopie aller Direktiven aus dem Hauptmodul
  2. Zusätzliche Direktiven für das Testen

 

Notiz: Das Kopieren von Direktiven aus dem Hauptmodul is natürlich etwas mühselig und fehleranfällig. Das Java-Build-Tool Pro automatisiert diesen Schritt und erlaubt es ausschließlich die zusätzlichen Direktiven fürs Testen anzugeben.

White Box Modular Testing with extra Java command line options

Neben der gerade beschriebenen Variante eines dediziertem Testmoduls, kann man auch mittels java Kommandozeilenparameter ans Ziel kommen. Man konfiguriert so das Modulsystem quasi beim Starten der JVM. Die meisten Build-Tools unterstützen die Angabe solcher java Kommandozeilen-Parameter beim Starten eines Testlaufs.

Um obiges Testmodul mittels Java nachzubauen, braucht es folgende Parameter:

Zusammenfassung und ein Beispiel

  • Wie organisiert man nun Tests in modularen Projekten?

Es ist in meine Augen notwendig, dass sowohl Black Box als auch White Box Tests geschrieben und ausgeführt werden. Das micromata/sawdust-Projekt zeigt eine mögliche Struktur:

Dazu kommt noch, dass man das Verhalten seines Moduls sowohl auf class-path als auch module-path testen sollte.

Die englische Textversion dieses Blogbeitrags gibt es hier.

Christian Stein