Uplift

Migration von JUnit 4 nach 5

Dieser Artikel richtet sich an Programmierer, die ihre automatisierten Tests von JUnit 4 auf JUnit 5 erweitern wollen.

JUnit ist in der Java-Welt als Modultest-Tool weit verbreitet. Es kann nicht nur für Unit-Tests, sondern mit Hilfe weiterer Bibliotheken für alle Arten von Tests eingesetzt werden. Im September 2017 wurde JUnit 5 (Codename: „Jupiter“) veröffentlicht und seitdem weiterentwickelt.

Waren bei JUnit 4 die Neuerungen aus Java 5 – hauptsächlich Annotationen – maßgeblich für die Weiterentwicklung, sind es nun Elemente aus Java 8, allen voran Lambdas und Streams, die eine erneute Überarbeitung angestoßen haben.

Diese Reihe von Artikeln soll einen Überblick und Hilfestellung dabei geben, mit geringem Aufwand auf die neue Version von JUnit wechseln zu können, und vorhandene Tests zu erweitern.

Was ändert sich?

Wie bei den Vorgänger-Versionen ändert sich die Paketstruktur der JUnit API. Waren bis Version 4 noch alle Klassen in einer .jar-Datei gebündelt, so wurden diese nun auf mehrere Pakete für platform, engine und api aufgeteilt. Hintergrund ist, dass die Test-APIs nun mit verschiedenen Engines verwendet werden, (z.B. der Vintage-Engine für JUnit 3 & 4) oder sogar neue Engines (z.B. für TestNG) auf der Basis der Jupiter-Plattform hinzugefügt werden können.

Um bestehende JUnit-Tests auf dem JUnit 5 Runner laufen zu lassen, muß lediglich die Abhängigkeit (Dependency) in Maven oder Gradle geändert werden von:

junit:junit:4.12

zu :

org.junit.vintage:junit-vintage-engine:5.3.1

Hierfür sind keinerlei Änderungen an den Testklassen selbst notwendig.

Sollen nun neue Tests auf Basis von Version 5 entwickelt werden, wird folgende Abhängigkeit benötigt:

org.junit.jupiter:junit-jupiter-api:5.3.1

Normalerweise kümmert sich Gradle oder Maven um den Rest, d.h. das surefire-plugin lädt automatisch Platform und Engine nach, um die Tests auszuführen.

Lifecyle Annotations

Der klassische Lebenszyklus der Testmethoden einer Klasse ist unverändert geblieben, allerdings wurden die Namen etwas eindeutiger benannt:

JUnit 4 JUnit 5 Funktion
@BeforeClass @BeforeAll Ausführung vor allen Testmethoden
@Before @BeforeEach Ausführung vor jeder Testmethode
@Test @Test Der/die Test(s) selbst
@After @AfterEach Ausführung nach jeder Testmethode
@AfterClass @AfterAll Ausführung nach allen Testmethoden

Nach der Umbenennung aller Annotations ist aus einer einfachen JUnit 4- bereits eine vollwertige JUnit 5-Klasse geworden. Dabei darf man nicht vergessen, die Importe von @Test und den Asserts anzupassen:

import org.junit.jupiter.api.Test

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

Zusätzlich können jetzt übrigens alle Methoden und die Klasse selbst von public zu package-private gemacht werden. Moderne IDEs weisen oft in ihrer Codeanalyse direkt darauf hin. (Lint etc.)

Weiterhin ist es nun möglich, die @BeforeAll/@AfterAll-Methoden, die per default statisch sein müssen, nicht statisch zu machen, und eventuell davon abhängige globale Variablen zu Instanzvariablen zu ändern. Dies ist möglich durch Annotieren der Klasse mit @TestInstance(Lifecycle.PER_CLASS) Das führt dazu, dass sämtliche Tests auf demselben Objekt ausgeführt werden, im Gegensatz zu Lifecycle.PER_METHOD, bei der für jeden @Test ein neues Objekt der Klasse angelegt wird

Was kommt hinzu?

Auch wenn die @Test-Methode sich äußerlich nicht verändert hat, gibt es dennoch ein paar Unterschiede zum Vorgänger. Die Annotation-Parameter expected und timeout werden nicht mehr verwendet. Dafür gibt es in JUnit 5 nun eigene Assertions. Diese enthalten unter anderem:

assertThrows(Exception.class, () -> { … });
assertTimeout(ofMinutes(2), () -> { … });
assertTimeoutPreemptively(ofMillis(10), () -> { … });

Eine weitere Assertion gibt uns nun die Möglichkeit, mehrere Asserts zu durchlaufen, ohne beim ersten Fehler den Test abzubrechen.

assertAll("assertgroup", () -> { … }, () -> { … }, … );

Allen anderen Assertions ist zudem gemein, dass die optionale Fehlermeldung nun als letzter Parameter und nicht, wie bei JUnit 4 als erster übergeben wird.
Dies kann nun auch über ein Lambda (in diesem Fall eine Funktion vom Typ Supplier<String>) geschehen, d.h. differenzierte Fehlermeldungen werden nur dann erstellt, wenn ein Fehler auftritt:

assertEquals(expected, actual, () -> getMessage() );

Hier kann man nun deutlich erkennen, welche Bedeutung die Lambdas für Junit 5 haben.
Ein weiteres Feature von Junit 5 ist, dass Testmethoden nun auch Parameter übergeben bekommen können. Ein Objekt, das in jeder Test-Methode abgerufen werden kann, ist eine Instanz von TestInfo, aus der folgende Informationen ausgelesen werden können:

getDisplayName()
getTestClass()
getTestMethod()
getTags()

Der Display-Name ist der Name des Tests, der durch die Annotation @DisplayName("Test Name") sowohl auf den Methoden als auch der Klasse beliebig gesetzt werden kann und im Testprotokoll verwendet wird.

Was fällt weg?

In Junit 5 gibt es keine Runner und keine Rules mehr. Stattdessen setzt man hier auf das sogenannte Extension Modell. Die Funktionalitäten für Suite, Enclosed, Categorized und Parameterized fehlen dennoch nicht. Sie wurden nur umbenannt und sind jetzt in jeder Test-Klasse, auch gemeinsam auf derselben Klasse, verfügbar.

JUnit 4 JUnit 5 Funktion
@RunWith(Suite.class) @SelectClasses(…)
@SelectPackages(…)
Gemeinsame Ausführung
@RunWith(Enclosed.class) @Nested Geschachtelte Tests
@RunWith(Category.class) @Tag("…") Test-Kategorien
@RunWith(Parameterized.class) @ParameterizedTest(…)
@RepeatedTest(…)
Wiederholende Tests

Was ebenfalls entfällt, ist die eingebundene Hamcrest-Bibliothek. Diese wurde in Junit 4 noch in der .jar-Datei zusammen ausgeliefert, ist aber aufgrund der Aufteilung von JUnit 5 in mehrere Pakete nicht mehr automatisch dabei. Man kann sie  jederzeit selbst einbinden und so sicherstellen, immer die aktuelle Version, wie z.B. das neue java-hamcrest zu verwenden:

org.hamcrest:java-hamcrest:2.0.0.0

Abschließend bleibt noch zu erwähnen, dass Tests, die für den Testlauf ausgeblendet werden sollen, statt mit @Ignore mit @Disabled("Reason why …") annotiert werden.