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.

 

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 als eingefleischter Eclipse Benutzer derzeit noch Maven, den neuen JUnit Console Launcher oder die JUnitPlatform-Lösung.

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:

<Datum und Zeit der Ausführung> org.junit.vintage.engine.discovery.JUnit4DiscoveryRequestResolver lambda$loggingPotentialJUnit4TestClassPredicate$4
INFORMATION: Class de.mike.training.junit5.FirstTestDemo could not be resolved
╷
├─ JUnit Jupiter <img alt="ok" 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">
│  └─ FirstTestDemo <img alt="ok" 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">
│     └─ myFirstTest() <img alt="ok" 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">
└─ JUnit Vintage <img alt="ok" 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">

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:
Eclipse Output für JUnit Platform Runner

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, () -&gt; "1 + 1 should equal 2");

assertAll("Check all assertions and report each",
() -&gt; assertTrue(1 + 2 == 5, "1 + 2 should equal 3"),
() -&gt; assertTrue(1 + 3 == 5, "1 + 3 should equal 4"),
() -&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.

Assertion mit Supplier

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", 
      () -&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
       */
       () -&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

  • Für jeden JUnit 4 Test ist folgendes zu ändern:
    • Statt import org.junit.Test; schreiben wir import org.junit.jupiter.api.Test;
  • Falls es einen JUnit 4 Test mit Mockito gibt, …
    • muss eine MockitoExtension existieren, die man über die Dokumentation erhält.
    • muss man auf die Mockito Core Version 2.* upgraden.
  • 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"));