© rawf8/Shutterstock.com
Drei neue Featurereleases

Neues von JUnit


Im September 2017 wurde die Version 5.0 von JUnit nach fast zweijähriger Entwicklung endlich fertiggestellt. Das JUnit-Team hat die Arbeit danach jedoch keineswegs eingestellt, sondern neben einigen Bugfix-Releases mittlerweile drei neue Featurereleases veröffentlicht.

Nach einer kurzen Einführung in das neue Programmiermodell für Tests konzentrieren wir uns in diesem Artikel auf die Neuerungen in den Featurereleases (zum Beispiel Kotlin-Support, Tag Expressions und parallele Testausführung) und lernen, dass JUnit 5 viel mehr ist, als ein Testing-Framework für Java.

Das neue Programmiermodell

Das mit JUnit 5.0 vorgestellte neue Programmiermodell zum Schreiben von Tests und Erweiterungen trägt den Namen JUnit Jupiter API. Da das JUnit-Team bezweckt, dieses API über mehrere Major-Versionen hinweg weiterzuentwickeln, enthält der Name bewusst keine Versionsnummer. Eine kleine Referenz zur Zahl Fünf gibt es dennoch, denn Jupiter ist, von der Sonne aus gezählt, der fünfte Planet in unserem Sonnensystem.

Listing 1: Eine erste Testklasse

import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; class CalculatorTests { private Calculator calculator; @BeforeEach void createCalculator() { calculator = new Calculator(); } @Test @DisplayName("1 + 1 = 2") @Tag("addition") void onePlusOneIsTwo() { long newValue = calculator.set(1).add(1).longValue(); assertEquals(2, newValue); } @Test @DisplayName("(2 * 3) / 4 = 6/4 = 3/2 = 1.5") @Tag("multiplication") @Tag("division") void divideResultOfMultiplication() { BigDecimal newValue = calculator.set(2).multiply(3).divide(4).get(); assertEquals(new BigDecimal("1.5"), newValue); } @Test @Tag("input-validation") void cannotSetValueToNull() { IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> calculator.set(null)); assertEquals("cannot set value to null", exception.getMessage()); } }

Am besten lässt sich das neue Programmiermodell anhand eines Beispiels erklären. Dazu betrachten wir Unit -Tests für eine Calculator-Klasse, die einen einfachen Taschenrechner repräsentiert (Listing 1). Eine Calculator-Instanz hat jederzeit einen aktuellen Wert, der durch Anwendung einer Rechenoperation, d. h. den Aufruf einer Methode, modifiziert wird. Alle Codebeispiele in diesem Artikel sind auf GitHub verfügbar [1].

Auf den ersten Blick sieht die Testklasse sehr ähnlich aus wie eine, die auf JUnit 4 basiert. So werden Testmethoden weiterhin mit @Test annotiert. Auf den zweiten Blick fällt allerdings auf, dass die Annotationen nun aus dem org.junit.jupiter.api-Package und nicht aus org.junit importiert werden. Des Weiteren müssen Testklassen und -methoden in Jupiter nicht mehr public sein, die Standardsichtbarkeit genügt.

Wie man anhand der createCalculator()-Methode sehen kann, gibt es weiterhin eigene Annotationen für Methoden der Testklasse, die am Lebenszyklus eines Tests teilnehmen möchten. Allerdings haben sie leicht andere Namen als in JUnit 4 (zum Beispiel @BeforeEach statt @Before).

philipp_junit_1.tif_fmt1.jpgAbb. 1: Ausführung von „CalculatorTests“ in IntelliJ IDEA

Wie die onePlusOneIsTwo()-Methode zeigt, ist man beim Benennen der Tests nun nicht mehr auf die Syntax von Methodennamen in Java beschränkt, sondern kann mit Hilfe der @DisplayName-Annotation beliebige Zeichenketten verwenden. Dennoch kann man in IDEs direkt von dem Eintrag im Testbaum (Abb. 1) zur Methode navigieren.

Das Jupiter API bietet mit der Assertions-Klasse eine Basismenge an Assertions an, die so manchem (wie etwa assertEquals()) ebenfalls von JUnit 4 bekannt vorkommen dürfte. Wie man in der cannotSetValueToNull()-Methode sieht, gibt es darüber hinaus aber auch neue Assertions. Mit assertThrows() lässt sich zusichern, dass ein bestimmter Codeabschnitt (hier calculator.set(null)) eine gewisse Art von Exception wirft (hier IllegalArgumentException). Der Rückgabewert ist die tatsächlich aufgetretene Exception und kann verwendet werden, um weitere Assertions an Eigenschaften (hier die erwartete Message) zu formulieren. Das JUnit-Team empfiehlt Entwicklern, denen die in der Assertions-Klasse enthaltenen Methoden nicht ausreichen, ausdrücklich, Bibliotheken wie AssertJ [2] oder Hamcrest [3] zu verwenden.

Anstelle von Categories bietet Jupiter @Tags, die nun ebenfalls auf Strings basieren. Wie wir später sehen werden, sind Tags nützlich zur Filterung der auszuführenden Tests.

Parametrisierte Tests

Neben klassischen Testmethoden, die üblicherweise das Resultat einer beispielhaften Eingabe gegen eine erwartete Ausgabe prüfen, bietet das JUnit Jupiter API weitere Möglichkeiten, Tests zu formulieren. Diese sind insbesondere dann hilfreich, wenn man denselben Code mit vielen verschiedenen Eingaben testen möchte. Eine solche Variante stellen parametrisierte Tests dar.

Listing 2: Parametrisierter Test

@ParameterizedTest(name = "sqrt({0}) = {1}") @CsvSource({ "1, 1.0000000000000000", "2, 1.4142135623730951", "3, 1.7320508075688772", "4, 2.0000000000000000" }) @Tag("sqrt") void sqrt(long input, double expectedResult) { double actualResult = calculator.set(input).sqrt().doubleValue(); assertEquals(expectedResult, actualResult, 1e-16); }

Um einen parametrisierten Test zu schreiben, annotiert man die Testmethode mit @ParameterizedTest anstelle von @Test, wie etwa an der sqrt()-Methode im Beispiel (Listing 2). Zusätzlich muss man mindestens eine Source-Annotation deklarieren, die angibt, woher die Werte für die Parameter gelesen werden sollen. Im Beispiel verwenden wir @CsvSource und definieren die Parameter als kommagetrennte Liste. JUnit konvertiert die Strings automatisch in die deklarierten Parametertypen. Die Konvertierung lässt sich über eine zusätzliche @ConvertWith-Annotation anpassen. Details dazu findet man im JUnit 5 User Guide [4].

Die Testmethode wird einmal pro Parameterliste ausgeführt. Jede Ausführ...

Neugierig geworden?

Angebote für Teams

Für Firmen haben wir individuelle Teamlizenzen. Wir erstellen Ihnen gerne ein passendes Angebot.

Das Library-Modell:
IP-Zugang

Das Company-Modell:
Domain-Zugang