Headerbild für JUnit 5 – Extensions

Mit eigenen Erweiterungen zu fein gesteuerten Tests

Teil 3: JUnit 5 – Extensions

Von: Sven Ruppert

Mit dem Extension-Mechanismus stellt JUnit 5 ein leistungsfähiges und flexibles Werkzeug zur Verfügung, um Tests in Java auf eine modulare und wiederverwendbare Weise zu gestalten. Während JUnit 4 die Nutzung von Regeln und Runnern vorsah, wurde mit der neuen Architektur von JUnit 5 ein wesentlich differenzierteres Konzept eingeführt. Der neue Mechanismus erlaubt eine feinere Steuerung der Testausführung und eröffnet Entwicklern die Möglichkeit, eigene Erweiterungen zu definieren, die sich nahtlos in den Lebenszyklus von Tests integrieren lassen.

Der Extension-Mechanismus basiert auf der Schnittstelle org.junit.jupiter.api.extension.Extension, die als Marker-Interface fungiert. Für spezifische Anwendungsfälle gibt es verschiedene Interfaces wie BeforeAllCallbackAfterAllCallbackBeforeEachCallback  oder  AfterEachCallback, die in Abhängigkeit von den Anforderungen implementiert werden können. Diese feingranulare Kontrolle über den Testlebenszyklus ermöglicht eine gezielte Modifikation des Testverhaltens, sei es durch das Einfügen einer Setup- und Teardown-Logik, das Manipulieren von Testdaten oder das Konfigurieren von Testumgebungen.

Ein besonders nützliches Einsatzszenario für JUnit 5 Extensions ist das Injizieren von Abhängigkeiten in Testklassen. Mit der Einführung von ParameterResolver können Entwickler Testmethoden definieren, die auf externe Ressourcen zugreifen, ohne sich explizit um deren Instanziierung kümmern zu müssen. Der ParameterResolver prüft zur Laufzeit, ob für eine bestimmte Methode ein passender Parameter bereitgestellt werden kann, und stellt diesen dann entsprechend zur Verfügung. Dies ist insbesondere für die Integration mit Dependency Injection Frameworks wie Spring oder CDI von Vorteil, da Testfälle dadurch von der konkreten Erzeugung ihrer Abhängigkeiten entkoppelt werden.

Neben der Steuerung des Lebenszyklus und der Bereitstellung von Abhängigkeiten erlaubt der Extension-Mechanismus auch die Implementierung von bedingten Testausführungen. Durch das Interface ExecutionCondition kann beispielsweise spezifiziert werden, dass Tests nur unter bestimmten Bedingungen ausgeführt werden, etwa wenn eine bestimmte Systemvariable gesetzt ist oder eine bestimmte Konfiguration vorliegt. Dies bietet sich an, um Tests kontextabhängig auszuführen und beispielsweise betriebssystemspezifische oder plattformspezifische Tests zu realisieren.

Die folgende Implementierung zeigt eine einfache benutzerdefinierte JUnit 5 Extension, die vor und nach jedem Testfall eine Log-Ausgabe erzeugt. Diese Erweiterung implementiert die Interfaces BeforeEachCallback und AfterEachCallback, um sicherzustellen, dass der Code jeweils vor und nach der Testausführung ausgeführt wird:

import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.ExtensionContext;
import java.util.logging.Logger;

public class LoggingExtension implements BeforeEachCallback, AfterEachCallback {
    private static final Logger logger = Logger.getLogger(LoggingExtension.class.getName());

    @Override
    public void beforeEach(ExtensionContext context) {
        logger.info(() -> "Starting test: " + context.getDisplayName());
    }

    @Override
    public void afterEach(ExtensionContext context) {
        logger.info(() -> "Finished test: " + context.getDisplayName());
    }
}

Die so definierte Extension kann dann durch die Annotation @ExtendWith(LoggingExtension.class) in eine Testklasse integriert werden. JUnit 5 sorgt dafür, dass die definierten Methoden automatisch vor und nach jedem Testlauf aufgerufen werden, wodurch sichergestellt wird, dass relevante Informationen zur Testausführung protokolliert werden. Dies kann insbesondere bei der Fehlersuche und Analyse von Testläufen hilfreich sein.

Zusätzlich zu den grundlegenden Erweiterungen bietet JUnit 5 auch Mechanismen zur Steuerung der Ausführungsreihenfolge von Tests. Mithilfe der Annotation @TestMethodOrder kann eine explizite Reihenfolge für die Testausführung festgelegt werden. Es stehen verschiedene Strategien zur Verfügung, darunter OrderAnnotation für eine manuelle Reihenfolge mittels @OrderAlphanumeric für eine alphabetische Sortierung der Methodennamen und Random für eine zufällige Ausführungsreihenfolge. Dies ermöglicht eine gezielte Steuerung der Testreihenfolge, die insbesondere bei Integrationstests nützlich sein kann.

Ein weiteres wichtiges Feature ist die Möglichkeit, Tests parallel auszuführen. JUnit 5 unterstützt parallelisierte Tests durch die Konfiguration mittels der junit-platform.properties-Datei oder durch direkte API-Nutzung mit @Execution(ExecutionMode.CONCURRENT). Dies ermöglicht eine schnellere Testausführung und eine bessere Nutzung von Mehrkernprozessoren. Die Parallelisierung kann für Methoden innerhalb einer Testklasse oder über mehrere Klassen hinweg erfolgen, wodurch sich die Gesamtzeit der Testdurchläufe signifikant reduzieren lässt.

Ein weiteres leistungsfähiges Werkzeug innerhalb von JUnit 5 ist TestInfo. Dieses Interface ermöglicht es, Metadaten zu einem Testfall abzurufen, etwa den Namen des Tests, die zugehörigen Tags oder die Display-Namen. TestInfo wird als Parameter in Testmethoden injiziert und kann für Logging oder bedingte Testausführungen genutzt werden.

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInfo;

import static org.junit.jupiter.api.Assertions.*;

class TestInfoExample {
    @Test
    void testWithTestInfo(TestInfo testInfo) {
        assertNotNull(testInfo);
        System.out.println("Executing test: " + 
        testInfo.getDisplayName());
    }
}

Der neue Extensions-Mechanismus in JUnit 5 erlaubt es, die Testausführung in Java-Anwendungen äußerst flexibel zu steuern und zu erweitern. Durch die Trennung in unterschiedliche Extension-Typen und die Kombinierbarkeit mehrerer Extensions entsteht ein leistungsstarkes Werkzeug zur Modularisierung von Testlogik. Die Integration mit existierenden Frameworks und die Möglichkeit, domänenspezifische Erweiterungen zu definieren, macht den Mechanismus zu einem unverzichtbaren Bestandteil moderner Testentwicklung. In der Praxis zeigt sich, dass die konsequente Nutzung von JUnit 5 Extensions nicht nur die Wartbarkeit von Tests verbessert, sondern auch dazu beiträgt, eine bessere Trennung von Testlogik und Testkonfiguration zu erreichen. Durch die enge Verzahnung mit dem Testlebenszyklus erlaubt der Mechanismus eine hochgradig anpassbare und erweiterbare Testinfrastruktur, die sich flexibel an unterschiedliche Anforderungen anpassen lässt.

Happy Coding!

Über den Autor: Sven Ruppert

Sven programmiert seit 1996 Java in Industrieprojekten, seit über 15 Jahren weltweit Java in Branchen wie Automobil, Raumfahrt, Versicherungen, Banken, der UNO und der Weltbank. Seit zehn Jahren ist er als Sprecher auf Konferenzen und Community-Events in Ländern von Amerika bis Neuseeland. Er hat als Developer Advocate für JFrog und Vaadin gearbeitet und schreibt regelmäßig Artikel für IT-Magazine und Technologieportale.


Diese Beiträge könnten dich auch interessieren: