Maven Surefire und Probleme mit dem Classpath

In meinem letzten Projekt beim Kunden bin ich auf ein interessantes Problem im Maven Surefire Plugin gestoßen. Während meine Unit-Tests in der Eclipse-Umgebung problemlos liefen, warf Maven auf der Kommandozeile beim Aufruf von Class.forName() eine ClassNotFoundException. Was war da los?

Der zu testende Code

Die zu testende Code-Passage, die mit Maven Surefire und Junit 4.12 auf der Kommandozeile immer wieder fehlegschlagen ist, sah in etwa so aus:

final String className = "org.kivio.depot.Isin"
Class<?> clazz = (Class<?>)Class.forName(className);

An dieser Stelle zunächst nichts besonderes. Die Klasse Isin befindet sich allerdings in einem als Dependency deklarierten Jar-File, dass für den Test benötigt wird.

Die Besonderheit von Maven Surefire

Seit Maven Surefire 2.8.2 ist die Behandlung des Classpath und somit auch die Testausführung geändert worden. Standardmäßig ist der System Class Loader aktiv und Maven Surefire startet alle Tests mit einem Manifest-Only JAR.

Hierbei wird ein temporäres JAR erzeugt, dass nur den Inhalt META-INF/MANIFEST.MF enthält. In dem MANIFEST sind über das Attribut Class_Path alle notwendigen Abhängigkeiten für die Testausführung mit ihren absoluten Pfaden gesetzt.

Und hier kommen wir zum eigentlichen Problem: System Class Loader, Thread Context Class Loader und der Default Class Loader sind identisch und verweisen auf das Maven Surefire Booter JAR, das wiederum Einträge zu den JAR-Dateien mit Klassen enthält, die in einem anderen Kontext geladen worden sind.

Wenn eine Anwendung ihren Class Loader selbst nach JAR-Dateien oder Klassen befragt, ist es besser, wenn ein isolierter Class Loader genutzt wird, der alle Abhängigkeiten kennt. Das wäre in diesem Fall der korrekte Weg, um den Test zu starten und auf den System Class Loader zu setzen. Dies kann aber gerade im Embedded Kontext - also wenn Maven in einer IDE gestartet wird - wiederum zu anderen Problemen führen.

Ein weiterer Grund doch eher auf den Manifest-Only-Ansatz zu setzen, ist die Möglichkeit, dass Surefire Tests parallel ausführen kann und daher Forks vom Hauptprozess erstellt. Beim Forken scheinen allerdings die Class Loader durcheinander zu kommen und die Wiederverwendung eines Forks durch Surefire sollte unterbunden werden. Und mit exakt diesem Ansatz kann das Class Loading-Problem umgangen werden.

Ein Parameter bringt die Rettung

Eine einfache Änderung der Konfiguration des Surefire-Plugins sorgt dafür, dass Class.forName-Anweisungen im zu testenden Code funktionieren:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <configuration>
    <reuseForks>false</reuseForks>
  </configuration>
</plugin>

Mit reuseForks kann gesteuert werden, ob Surefire einen geforkten Prozess für einen weiteren Unit Test wiederverwenden soll. Durch die Unterbindung wird für jeden Testfall ein neuer Prozess gestartet und der Classpath korrekt gesetzt.

Allerdings hat dieses Vorgehen auch einen gravierenden Nachteil, der nicht verschwiegen werden sollte: Bei vielen durchzuführenden Tests kommt die Garbage Collection ggf. nicht nach und es kommt zu einem Überschreiten des GC Overhead Limits.

Die Option sollte also mit Vorsicht gesetzt werden.

Du möchtest diskutieren oder einen Kommentar zu dem Beitrag hinterlassen?

Dieser Blog hat keine öffentliche Kommentarfunktion, aber ich freue mich jederzeit über eine Mail mit kritischen Anmerkungen, Feedback oder auch einfach nur Lob an meine Mail-Adresse rollin.hand[@]gmx.de.