Ich werde in diesem Artikel meinen ganz persönlichen Migrationsguide beschreiben, wie ich jedes Projekt von JUnit 4 auf die JUnit 5 Plattform migriere.

Dafür benötige ich im Normalfalls 8 Schritte.
Beispielhaft habe ich dies in meinem Projekt arabic2roman Converter in einem separaten Branch durchgezogen.

  1. Step: Maven dependencies
  2. Step: Console Launcher für Testläufe
  3. Step: Ein Testfall wird erneuert (Smoke-Test)
  4. Step: Alle (kompatiblen) Tests werden erneuert.
  5. Step: Austausch der Rules
  6. Step: Umbau der Mockito-Tests
  7. Step: @Displayname kommt zum Einsatz
  8. Step: Neue Features & Freunde
  9. 1.Step: Change Maven dependencies

    Aus den alten Abhängigkeiten zu JUnit 4:

    <dependencies>
       ...
       <dependency>
          <groupId>junit</groupId>
          <artifactId>junit</artifactId>
          <version>4.11</version>
          <scope>test</scope>
          <exclusions>
             <exclusion>
                <groupId>org.hamcrest</groupId>
                <artifactId>hamcrest-core</artifactId>
             </exclusion>
          </exclusions>
       </dependency>
       ...
    </dependencies>
    

    werden drei neue Abhängigkeiten zu JUnit 5:

    <dependencies>
       ...
       <dependency>
          <groupId>org.junit.platform</groupId>
          <artifactId>junit-platform-launcher</artifactId>
          <version>1.1.0</version>
          <scope>test</scope>
       </dependency>
       <dependency>
          <groupId>org.junit.platform</groupId>
          <artifactId>junit-platform-console-standalone</artifactId>
          <version>1.1.0</version>
          <scope>test</scope>
       </dependency>
       <dependency>
          <groupId>org.junit.jupiter</groupId>
          <artifactId>junit-jupiter-engine</artifactId>
          <version>5.1.0</version>
          <scope>test</scope>
       </dependency>
       <dependency>
          <groupId>org.junit.vintage</groupId>
          <artifactId>junit-vintage-engine</artifactId>
          <version>5.1.0</version>
          <scope>test</scope>
       </dependency>
       ...
    </dependencies>
    

    Bis zur JUnit5 Version 5.0.* war die Version des junit-vintage-engine Artefakts noch nummeriert wie die letzte aktuelle JUnit 4 Version, also 4.12.*.
    Das wurde nun konsolidiert.

    2.Step: Console Launcher

    Danach kann man das Projekt in Eclipse oder IntelliJ mit dem Testrunner JUnit5 laufen lassen oder via mvn test oder – um die volle Bandbreite an Output der neuen Plattform zu genießen – via Console Runner.
    Dazu habe ich in meinem Beispielprojekt ein Unterverzeichnis consoleLauncher angelegt und den Console Standalone Runner hineinkopiert:

    java -jar consoleLauncher/junit-platform-console-standalone-1.1.0.jar 
         --classpath target/test-classes
                    :target/classes
                    :/%M2_REPO%/eu/codearte/catch-exception/catch-exception/1.4.4/catch-exception-1.4.4.jar
                    :/%M2_REPO%/org/objenesis/objenesis/1.2/objenesis-1.2.jar
                    :/%M2_REPO%/org/assertj/assertj-core/3.8.0/assertj-core-3.8.0.jar 
         -p de.neusta.kata.arabic2roman
    

    Der consoleLauncher kennt eine Menge unterschiedlicher Parameter, die allesamt ganz gut dokumentiert sind. Wichtig und – wie aus alten Java-KlassenpfadZeiten bekannt, sind natürlich die korrekten Angaben beim Klassenpfad:

    --classpath folder1:folder2:jar1:jar2
    

    Statt des Platzhalters %M2_REPO% muss hier natürlich der korrekte lokale Pfad aufgelöst werden, aber das ist Maven KnowHow, auf das ich hier nicht weiter eingehen möchte.

    3.Step: Putting one toe into the unknown water

    Jetzt ist es soweit: Wir könnten unseren ersten JUnit5 Test schreiben.

    Statt dessen könnte man aber auch einen der vorliegenden JUnit4 Tests anpassen. Dabei fällt mir sofort auf, dass ich natürlich in allen meinen Tests die Before-Methode aus JUnit4 bereits mitverwende.

    In JUnit5 sollte diese Methode mit @BeforeEach annotiert werden und so versuche ich mein Glück und annotiere meine setUp-Methode einfach zweimal und einen meiner Tests mit @:

    @Before
    @BeforeEach
    public void setUp() {
       converter = new Arabic2RomanConverter();
    }
    
    @org.junit.jupiter.api.Test
    public void testInvalidNegativeParameter() throws Exception {
       ...
    }
    

    …und siehe da: Es geht.

    So kann ich also mal fühlen, wie kalt das JUnit5 Haifischbecken wirklich ist.

    In meinem Konsolendurchlauf erkenne ich sofort den Unterschied:

    Vorheriger Output
    Vorheriger Output
    Nachheriger Output
    Nachheriger Output

    4.Step: Swim a little bit (without diving too deep)

    In diesem Schritt kann man nun alle Tests, die KEINE Rules, spezielle Runner und/oder Mockito verwenden, auf JUnit5 umstellen. Das habe ich in meinem Beispielprojekt auch so praktiziert.

    Man kann diesen Schritt natürlich auch überspringen und die Tests weiterhin unter JUnit4 laufen lassen, aber dies ist ja nicht im Sinne der Migration.

    5.Step: Forget the Rules

    Eine typische Rule, die häufig zum Einsatz kommt, ist ExpectedException: Mit dieser Rule lässt sich eine geworfene Exception nicht nur auf den Typ überprüfen, so wie mit:

    @Test(expected=IllegalArgumentException.class)
    

    sondern eben auch die zurückgegebene Message oder der (Root) Cause der Exception.

    Beispielsweise so wie hier:

    ...
    import org.junit.Rule;
    import org.junit.rules.ExpectedException;
    
    public class Arabic2RomanConverterWithoutTDDTest {
    
    ...
       @Rule
       public ExpectedException expectedException = ExpectedException.none();
    
       @Test
       public void testInvalidNegativeParameter() throws Exception {
          expectedException.expect(IllegalArgumentException.class);
          expectedException.expectMessage("Only numbers between 0 and 3000 are allowed.");
          converter.convert(-1);
       }
    ...
    

    Dies geht unter JUnit5 nun etwas einfacher und vor allem direkt mit Bordmitteln:

    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.junit.jupiter.api.Assertions.assertThrows;
    ...
    public class Arabic2RomanConverterWithoutTDDTest {
       ...
       @Test
       public void testInvalidNegativeParameter() throws Exception {
          Throwable exception = assertThrows(IllegalArgumentException.class, () -&gt; {
             converter.convert(-1);
          });
          assertEquals("Only numbers between 0 and 3000 are allowed.", exception.getMessage());
       }
    ...
    

    Für weitere Rules gibt es noch experimentellen Support.

    6.Step: Mocks are all around

    Natürlich ist man nur bei sehr kleinen Komponenten in der Lage auf Dependency Injection zu verzichten.
    Wenn ich also einen Service schreiben möchte, der beispielsweise mein Datum in römische Schreibweise konvertiert, so injeziere ich mir den Converter als Abhängigkeit, die ich weg“mocken“ kann:

    public class DateInRomanCharsService {
    
       @Resource
       private Arabic2RomanConverter converter;
    
       public String getDate(final Date date) {
          // use converter to return a roman variant of the given date
       }
    
    }
    

    Um diesen Service zu testen, verwende ich nun Mockito:

    <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
       <version>2.11.0</version>
       <scope>test</scope>
    </dependency>
    

    und schreibe folgenden Test:

    import static org.junit.jupiter.api.Assertions.assertEquals;
    import static org.mockito.Mockito.when;
    
    import java.text.SimpleDateFormat;
    
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    import org.mockito.InjectMocks;
    import org.mockito.Mock;
    import org.mockito.MockitoAnnotations;
    
    class DateInRomanCharsServiceTest {
    
       @InjectMocks
       private DateInRomanCharsService service;
    
       @Mock
       private Arabic2RomanConverter converter;
    
       @BeforeEach
       void setUp() {
          MockitoAnnotations.initMocks(this);
       }
    
       @Test
       void testServiceCallForCertainDate() throws Exception {
          String date = "25.04.1971";
          when(converter.convert(25)).thenReturn("XXV");
          when(converter.convert(4)).thenReturn("VI");
          when(converter.convert(1971)).thenReturn("MCMLXXI");
          assertEquals("XXV-VI-MCMLXXI", service.getDate(new SimpleDateFormat("dd.MM.yyyy").parse(date)));
       }
    }
    

    7.Step: Friendly names

    Eine der schönsten Features in JUnit5 ist sicherlich, die Möglichkeit, einen Test mit einem bildschirmgeeigneten Namen zu versehen.

    @Test
    @DisplayName("Numbers over 3000 throw an IllegalArgumentException.")
    public void testInvalidTooBigParameter() throws Exception {
       ...
    }
    

    Natürlich werde ich den Testmethoden trotz allem sprechende Bezeichner vergeben, die allerdings nun nicht mehr das komplette Testszenario umschreiben müssen, wie beispielsweise:

    testParameterGreaterThan3000ThrowAnIllegalArgumentException

    8.Step: Friendly Features

    Jetzt ist der Boden bereit für die Verwendung aller neuen Features in JUnit 5. Allen voran schiele ich dabei auf die Einführung parametrisierter Tests. Dies war zwar bereits in JUnit 4 möglich, aber umständlich und unleserlich oder nur mit zusätzlichen Bibliotheken (Beispiel: JunitParams).
    Um parametrisierte Tests in JUnit5 zu verwenden, braucht es allerdings auch eine weitere Dependency, da es sich dabei noch um ein Feature im experimentellen Zustand handelt:

    <dependency>
       <groupId>org.junit.platform</groupId>
       <artifactId>junit-platform-console-standalone</artifactId>
       <version>1.1.0</version>
       <scope>test</scope>
    </dependency>
    

    Anschließend kann man die entsprechende Annotationen an den Test hängen:

    import org.junit.jupiter.params.ParameterizedTest;
    import org.junit.jupiter.params.provider.CsvSource;
    ...
    @ParameterizedTest(name = "{0}  → \"{1}\"")
    @CsvSource({ "0, ''", 
                 "1, I", 
                 "2, II", 
                 "4, IV", 
                 "5, V", 
                 "9, IX", 
                 "40, XL", 
                 "90, XC", 
                 "400, CD", 
                 "500, D", 
                 "900, CM",
                 "1000, M", 
                 "1984, MCMLXXXIV", 
                 "2300, MMCCC", 
                 "2330, MMCCCXXX", 
                 "2350, MMCCCL", 
                 "3000,MMM" })
    @DisplayName("Konvertiere arabische Zahl in römische Zeichen")
    void testDifferentValues(final int arabicNumber, final String romanChars) throws Exception {
       assertThat(new Arabic2RomanConverter().convert(arabicNumber), is(romanChars));
    }