Seit Sommer 2015 wurde das Projekt JUnit Lambda gestartet, mit der Zielsetzung Java 8 Features in das Testing Framework JUnit zu integrieren. Aus dem Arbeitstitel JUnit Lambda wurde ein neues Major Release JUnit 5, das gegenüber der „alten“ Versionen völlig umkonzipiert wurde.
Wem es lieber ist, ein Youtube Video anzusehen, um diese neuen Features kennenzulernen, der hat u.a. hier die Möglichkeit:
Grundlegende Änderung
Test Definition
Die Testdefinition bzw. -deklaration läuft wie unter JUnit 4 mit einer Annotation @org.juni.jupiter.api.Test
.
Jedoch kennt diese Annotation keine Parameter (expected, timeout). Exception Handling und Timeouts werden anders behandelt.
package de.mike.training.junit5; import static org.hamcrest.CoreMatchers.is; import org.junit.jupiter.api.Test; import static org.junit.Assert.assertThat; class FirstTestDemo { @Test void myFirstTest() { assertThat("1 + 1 should equal 2", 1 + 1, is(2)); } }
Wie man vielleicht erkennen kann, sind weder die Klasse noch die Methode public, was unter JUnit 4 noch der Fall sein musste.
Bevor nun weitere Eigenschaften vorgestellt werden, wollen wir diesen ersten Test auch mal starten.
Start eines Test(lauf)s
Um einen Test mit JUnit 5 zum Laufen zu kriegen, benötigt man eine IDE wie Eclipse/IntelliJ oder Maven oder den neuen JUnit Console Launcher oder die JUnitPlatform-Lösung.
Eclipse Integration
Für die Integration in Eclipse Oxygen gab es eine Anleitung, die ich persönlich nicht nutzen konnte. Es hat nicht funktioniert. So musste ich warten, bis ein entsprechendes Plugin zur Verfügung stand, unter: https://www.eclipse.org/downloads/eclipse-packages.
Jetzt funktioniert es. Ich kann neue JUnit 5 Tests anlegen…
…und diese auch laufen lassen.
Console Launcher
JUnit 5 bringt mit ein eigenes Modul zum Start der neuen Tests mit:
junit-platform-console-standalone-1.0.0-M*.jar
Zu Testzwecken habe ich mir dieses JAR aus dem Maven Repository gezogen und es in einem Unterordner meines Projekts (./consoleLauncher) abgelegt.
Anschließend kann ich meine Tests mit folgendem Aufruf starten:
java -jar consoleLauncher/junit-platform-console-standalone-1.0.0-M5.jar --classpath target/test-classes:target/classes --include-classname ^.*Demo?$ --select-class de.mike.training.junit5.FirstTestDemo
Es handelt sich also um ein ausführbares JAR dem einige Optionen übergeben werden können.
Ich binde mit –classpath alle neben dem Standard-Klassenpfad notwendigen Verzeichnisse mit ein. Die einzelnen Verzeichnisse werden mit : miteinander verknüpft. Die Option –include-classname erlaubt mir, dass Standardtestnamensschema XYZTest bzw. XYZTests für meine Tests zu ändern. In diesem Fall auf XYZDemo.
Schließlich und endlich wähle ich noch die auszuführende Testklasse aus, indem ich der Option –select-class den vollqualifizierten Klassennamen meines JUnit Tests übergebe.
Wer alle Optionen einmal sehen möchte, ruft einfach auf:
java -jar consoleLauncher/junit-platform-console-standalone-1.0.0-M5.jar --help
Das Resultat ist schon mal ansprechender als es unter JUnit 4.*/3.* war:
&amp;lt;Datum und Zeit der Ausführung&amp;gt; org.junit.vintage.engine.discovery.JUnit4DiscoveryRequestResolver lambda$loggingPotentialJUnit4TestClassPredicate$4 INFORMATION: Class de.mike.training.junit5.FirstTestDemo could not be resolved ╷ ├─ JUnit Jupiter &lt;img alt="ok" draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="✔" src="https://s.w.org/images/core/emoji/2.2.1/svg/2714.svg">" src="https://s.w.org/images/core/emoji/2.2.1/svg/2714.svg">" src="https://s.w.org/images/core/emoji/2.2.1/svg/2714.svg"&gt; │ └─ FirstTestDemo &lt;img alt="ok" draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="✔" src="https://s.w.org/images/core/emoji/2.2.1/svg/2714.svg">" src="https://s.w.org/images/core/emoji/2.2.1/svg/2714.svg">" src="https://s.w.org/images/core/emoji/2.2.1/svg/2714.svg"&gt; │ └─ myFirstTest() &lt;img alt="ok" draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="✔" src="https://s.w.org/images/core/emoji/2.2.1/svg/2714.svg">" src="https://s.w.org/images/core/emoji/2.2.1/svg/2714.svg">" src="https://s.w.org/images/core/emoji/2.2.1/svg/2714.svg"&gt; └─ JUnit Vintage &lt;img alt="ok" draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="<img draggable="false" data-mce-resize="false" data-mce-placeholder="1" data-wp-emoji="1" class="emoji" alt="✔" src="https://s.w.org/images/core/emoji/2.2.1/svg/2714.svg">" src="https://s.w.org/images/core/emoji/2.2.1/svg/2714.svg">" src="https://s.w.org/images/core/emoji/2.2.1/svg/2714.svg"&gt; Test run finished after 32 ms [ 3 containers found ] [ 0 containers skipped ] [ 3 containers started ] [ 0 containers aborted ] [ 3 containers successful ] [ 0 containers failed ] [ 1 tests found ] [ 0 tests skipped ] [ 1 tests started ] [ 0 tests aborted ] [ 1 tests successful ] [ 0 tests failed ]
Natürlich kann ich mein komplettes Projekt (inkl. aller Tests) ausführen lassen, wofür ich den folgenden Befehl benutze:
java -jar consoleLauncher/junit-platform-console-standalone-1.0.0-M5.jar --classpath target/test-classes:target/classes --scan-classpath --include-classname ^.*Tests?$ --include-classname ^.*Demo?$
Die Option –scan-classpath scannt nun den kompletten Klassenpfad nach Tests, die wiederum auf Test (–include-classname ^.*Tests?$) oder Demo (–include-classname ^.*Demo?$) enden.
@RunWith(org.junit.platform.runner.JUnitPlatform.class)
Es gibt aber auch, die recht einfache Methode, Tests mit einem speziellen Runner laufen zu lassen, nämlich org.junit.platform.runner.JUnitPlatform
.
Der Runner ist für die Ausführung von Tests mit Systemen (IDEs, Buildsysteme, …), die eine Unterstützung der JUnit Plattform noch nicht gewährleisten, gedacht und insofern ein guter Kandidat für mich als Eclipse User.
Die Ausgabe sieht wie folgt aus:
Assertions
Natürlich gibt es die gleichen Assertions wie unter JUnit 4 und – die gute Nachricht – auch die hamcrest Assertions können weiterverwendet werden.
Assertions mit Supplier
Bei den Assertions sind aber neue hinzugekommen, um die Java8 Funktionalität eines Suppliers zu nutzen.
@Test
void myFirstTestWithSupplier() {
assertTrue(1 + 1 == 2, () -&amp;gt; "1 + 1 should equal 2");
assertAll("Check all assertions and report each",
() -&amp;gt; assertTrue(1 + 2 == 5, "1 + 2 should equal 3"),
() -&amp;gt; assertTrue(1 + 3 == 5, "1 + 3 should equal 4"),
() -&amp;gt; assertTrue(2 + 2 == 5, "2 + 2 should equal 4"));
}
Die erste Prüfung assertTrue nutzt einen Supplier als zweiten Parameter, so dass besonders komplexe Fehlermeldungen erst ausgewertet werden müssten, wenn der Fehler auftritt. So bleiben grüne Tests sehr performant.
Die zweite Prüfung assertAll erlaubt nun eine ganze Reihe von Prüfungen (Assertions) durchzuführen, die allesamt auch geprüft werden, um am Ende das Gesamtergebnis auszugeben.
Des Weiteren können natürlich nun auch mehrere Prüfungen in einem Block zusammengefasst werden.
@Test void testMyPerson() { Person me = new Person(); assertAll("It's not me, because", () -&amp;gt; { /* * Diese beiden Prüfungen werden abhängig durchgeführt */ assertThat(me.getFirstname(), is("Michael")); assertThat(me.getLastname(), is("Albrecht")); }, /* * Hingegen diese Prüfung wird aufgrund des assertAll in jedem Fall * gemacht */ () -&amp;gt; assertNotNull("not born yet", me.getBirthdate()); }
In diesem Fall kann man schön sehen, dass zwei Prüfungen ganz normal, also sequentiell und abhängig geprüft werden, nämlich der Vor- und Nachname. Hingegen wird die Datumsprüfung in jedem Fall – unabhängig von der Namensprüfung – durchgeführt.
So sind also beliebig komplexe Ab- und Unabhängigkeitsprüfungen denkbar. Man muss nur die Blöcke im Auge behalten.
Migration von JUnit 4 auf JUnit 5
Step-by-step Guide
- Allen voran benötigen wir die neuen Bilbiotheken in unserem Projekt.
Für ein Maven Projekt hieße dies beispielsweise folgende Dependencies einzubauen:<properties> ... <junit.version>4.12</junit.version> <junit.jupiter.version>5.0.0</junit.jupiter.version> <junit.vintage.version>${junit.version}.0</junit.vintage.version> <junit.platform.version>1.0.0</junit.platform.version> </properties> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>${junit.jupiter.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-commons</artifactId> <version>${junit.platform.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-engine</artifactId> <version>${junit.platform.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-launcher</artifactId> <version>${junit.platform.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-runner</artifactId> <version>${junit.platform.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.platform</groupId> <artifactId>junit-platform-console-standalone</artifactId> <version>${junit.platform.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency>
- Wenn wir die Tests nun bereits laufen ließen, sähen wir folgendes:
╷
├─ JUnit Jupiter ✔
└─ JUnit Vintage ✔
├─ de.mike.kata.MyFirstTest ✔
│ └─ test ✔
├─ de.mike.kata.arabic2roman.ConverterTest ✔
… - Für jeden JUnit 4 Test ist folgendes zu ändern:
- Statt
import org.junit.Test;
schreiben wirimport org.junit.jupiter.api.Test;
- Statt
- Falls es einen JUnit 4 Test mit Mockito gibt, …
- muss man statt mit einer Rule oder einem Runner zu arbeiten, die Methode:
MockitoAnnotations.initMocks(this);
in einer mit @BeforeEach annotierten Methode aufrufen.
- muss man statt mit einer Rule oder einem Runner zu arbeiten, die Methode:
- Nachdem diese Extension erstellt wurde bzw. falls sie existiert, muss man für jeden JUnit 4 Test mit @RunWith(MockitoJUnitRunner.class) folgendes tun:
- Ersetze @RunWith(…) durch @ExtendWith(<your>.<package>.<YourMockitoExtensionName>.class)
- Jeder Test bzw. jede vorbereitende Methode, die einen Mock verwendet, erhält diesen als Funktionsparameter:
public void testWhatYouWant(@Mock MyClass2Mock myMock) throws Exception { when(myMock.doAnything()).thenReturn(...)
- Falls ein JUnit 4 Test eine ExpectedException Rule verwendet, muss diese entfernt werden und an den Stellen, an den eine Exception erwartet wird, kann folgendes Code Fragment helfen:
Throwable exception = assertThrows(MyExpectedExceptionType.class, () -> { callMethodLeadingToException(); }); assertThat(exception.getMessage(), is("Fehler"));