Migration zu JUnit 5

Nach langem Beta-Test ist im September JUnit 5 erschienen. Es wurde von Grund auf neu und mit dem Ziel entwickelt, das Testen auf der JVM an die neuen Features von Java 8 und Lambdas anzupassen und auch andere Arten des Testens, wie z.B. behavior-driven-development, zu ermöglichen.

Seitdem sind eine Menge Tutorials erschienen, die den Einstieg erklären. Dabei fällt jedoch auf, dass die Autoren des Frameworks auch viele alte Zöpfe abgeschnitten haben, sodass die Migration in Projekten mit vielen schon existierenden Tests nicht ganz einfach ist. Wir möchten in diesem Beitrag zeigen, wie die Einführung dennoch klappt.

Einführung

Anders als Version 4 ist JUnit 5 nicht mehr nur eine große Library. Im User-Guide wird die neue Architektur so erklärt:

JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage

Die JUnit-Plattform ist die Laufzeit, in der die Tests ausgeführt werden. Sie definiert eine API, gegen die Test-Engines implementiert werden (dazu gleich mehr) und liefert die Werkzeuge mit denen sich Tests starten lassen, wie z.B. ein Maven- oder Gradle-Plugin.

JUnit Jupiter ist die Schnittstelle, gegen welche wir Entwickler unsere Tests implementieren. Hier liegen bekannte Annotationen wie @Test, BeforeEach oder @AfterAll (die in Junit4 noch @Before und @AfterClass hießen). Außerdem gehört hierzu eine Schnittstelle zur Erweiterung der Tests (ähnlich der aus Version 4 bekannten Rules oder Runner) und die eigentliche Test-Engine, welche die Tests ausführt, sobald sie von der Plattform angestoßen werden.

Hinter Vintage steckt ein Kompatibilitätslayer zur alten Version 4 oder 3: eine Test-Engine, welche existierende Tests ausführen kann, ohne dass Anpassungen im Code notwendig werden. Da die Plattform auch mit mehr als einer Engine umgehen kann, klappt das sogar parallel mit der Jupiter-Engine.

Dank dieser Struktur ist es möglich, neue Tests schon mit JUnit5 zu schreiben, ohne zuvor alle existierenden migrieren zu müssen.

Migration

Legen wir los: JUnit 5 lässt sich aktuell am einfachsten mit Gradle oder Maven verwenden. Da wir von einem existierenden Projekt ausgehen, zeigen die Code-Beispiele in diesem Artikel Maven. Zuerst fügen wir die Plattform zur POM hinzu:

<plugins>
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.19.1</version>
<dependencies>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-surefire-provider</artifactId>
<version>1.0.3</version>
</dependency>
<dependency>
<!-- allows running tests from older versions -->
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>4.12.3</version>
</dependency>
<dependency>
<!-- runs new JUnit 5 tests -->
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.0.3</version>
</dependency>
</dependencies>
</plugin>
</plugins>

Hier fügen wir zum Maven-Surefire-Plugin die JUnit-Platform und beide Engines hinzu (Vintage für ältere, und Jupiter für neue Tests).

Nun brauchen wir in den Dependencies noch die API für neue Tests. Die alte JUnit 4 Bibliothek wird weiterhin für bestehende Tests gebraucht:

<dependencies>
<dependency>
<!-- new Junit-API -->
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<!-- keep your old junit until you finished migrating -->
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

Ein erster Test

Nun können wir einen ersten Test gegen die neue API bauen:

Wie man sieht liegen die Annotation allesamt im Package org.junit.jupiter.api — hier gilt es aufzupassen, um im Parallelbetrieb nichts zu vermischen. Einige Annotationen haben sprechendere Namen bekommen, an die man sich schnell gewöhnt.

Bei den visibility-Levels gibt sich JUnit 5 nun genügsamer: Es reicht aus, Klassen und Methoden package-local (also ohne Modifier) anstatt wie bisher public zu deklarieren, was den Code angenehm kurz hält.

Nachdem JUnit 4 bei den Assertions auf Hamcrest (mit assertThat) gesetzt hat, bringt JUnit 5 diese Pakete nicht mehr mit. Wer lieber Hamcrest, Fest oder Assert4J benutzt, kann diese Bibliotheken trotzdem einfach einbinden und weiterverwenden.

Der letzte Test soll einen kleinen Ausblick auf neue Features geben: in der Test-Annotation gibt es den expected-Parameter zum Abfangen von Exceptions nicht mehr, stattdessen können Lambdas verwendet werden. Dank der DisplayName-Annotation sind der Kreativität bei der Vergabe sprechender Testnamen keine Grenzen mehr gesetzt.

Tool-Integration

Wenn wir die Tests nun mit mvn test starten, sehen wir, dass sowohl die neuen, als auch die alten Tests ausgeführt werden. In IntelliJ werden die Ergebnisse sogar nach der Engine (Jupiter und Vintage) getrennt.

Jupiter und Vintage Tests in IntelliJ IDEA

Da sich Maven-Surefire wie gewohnt um die Testausführung und das Reporting kümmert, muss am CI-Server nichts weiter angepasst werden.

Bei der IDE-Integration wird es schon schwieriger: bisher unterstützen nur IntelliJ IDEA und Eclipse Junit 5. IntelliJ war hier schon früh dabei, ältere Versionen enthalten aber z.T. noch frühe Milestones des Frameworks, so dass ggf. ein Update notwendig ist. Alternativ hilft ein Workaround aus dem User-Guide, oder man begnügt sich mit Maven/Gradle oder dem Console-Launcher.

Fazit und Ausblick

Mit JUnit 5 hat sich viel getan — wer den Aufwand einer kompletten Migration scheut, kann problemlos parallel mit der alten und neuen Versionen testen.

Auf Dauer wird das Hantieren mit ähnlichen Annotationen aus unterschiedlichen Packages jedoch unübersichtlich und man möchte die neuen, lieb gewonnenen Features auch in älteren Testklassen nutzen.

Wie sich existierende Tests Stück-für-Stück auf die neue API heben lassen, erklären wir in Teil 2.

Beispielcode

Der komplette Beispielcode zu diesem Artikel liegt auf Github: