Migration JUnit 4 zu JUnit 5 (mit Bezug auf Spring Boot)
- von Autor
Michael Wellner, Softwareentwickler bei sidion
JUnit 5 und Spring Boot
Wie oben beschrieben, verwendet Spring Boot mit seiner spring-boot-starter-test
Dependency seit der Version 2.2.0 JUnit 5. Aus org.junit
wird org.junit.jupiter.api
. Damit man aber nicht sofort mit den Breaking Changes konfrontiert wird, gibt es als default noch die junit-vintage-engine
, welche es ermöglicht, alte JUnit 4 Tests weiterhin laufen zu lassen. Möchte man allerdings eine aktuellere Version von Spring Boot verwenden (zum Zeitpunkt des des Schreibens ist Spring Boot 2.5.3 die aktuelle Version) ist die Vintage Engine nicht mehr mit an Board. Diese wurde ab 2.4.0 entfernt, sie kann aber jederzeit als separate Dependency hinzugefügt werden. Was muss man nun aber bei der Migration von JUnit 4 auf JUnit 5 alles beachten und wie geht man vor?
Migration
Unterstützung IntelliJ
In IntelliJ gibt es mehrere Möglichkeiten, sich unterstützen zu lassen.
-
Einerseits kann man sich eine zusätzliche Inspection anschalten namens
JUnit 4 test can be JUnit 5
, damit werden Tests markiert, welche ohne Probleme JUnit 5 Tests sein können (außerdem kann man sich hier auch noch ganz andere Inspections dazu schalten, die ich empfehlen kann - z. B. wenn man keine Assertion in einem Test hat - das mag Sonar nämlich auch nicht). -
Wurden nun Tests über die Inspection markiert, kann man hier einfach mittels
Migrate to JUnit 5
IntelliJ die Arbeit übernehmen lassen.
Manuelles Vorgehen
Bei unterstützter Migration mittels IntelliJ empfehle ich auf jeden Fall dann das git-Diff anzuschauen, um die Unterschiede zu erkennen. Die IntelliJ Migration kann aber nicht alles abfangen, bei einigen Dingen ist auch manuelle Arbeit notwendig. Meine Erfahrungen habe ich hier notiert - außerdem auch die Code-Stellen, die man 1:1 ersetzen kann (das was dann auch IntelliJ für einen machen kann).
Folgende Imports können 1:1 im Code ersetzt werden
Achtung: Bei statischen Methoden muss es nicht zwangsläufig sein, dass man das über Imports machen kann, falls man mal keine statischen Imports in einer Klasse hat.
-
import org.junit.Test;
mitimport org.junit.jupiter.api.Test;
-
import org.junit.Before;
mitimport org.junit.jupiter.api.BeforeEach;
-
import org.junit.After;
mitimport org.junit.jupiter.api.AfterEach;
-
import org.junit.BeforeClass;
mitimport org.junit.jupiter.api.BeforeAll;
-
import org.junit.AfterClass;
mitimport org.junit.jupiter.api.AfterAll;
-
import org.junit.runner.RunWith;
mitimport org.junit.jupiter.api.extension.ExtendWith;
-
import org.mockito.junit.MockitoJUnitRunner;
mitimport org.mockito.junit.jupiter.MockitoExtension;
-
import org.springframework.test.context.junit4.SpringRunner;
mit
-
import org.springframework.test.context.junit.jupiter.SpringExtension;
-
import static org.junit.Assert.
mitimport static org.junit.jupiter.api.Assertions.
-
import org.junit.Ignore;
mitimport org.junit.jupiter.api.Disabled;
Folgende Annotations können 1:1 im Code ersetzt werden
-
@Before
mit@BeforeEach
-
@After
mit@AfterEach
-
@BeforeClass
mit@BeforeAll
-
@AfterClass
mit@BAfterAll
-
@RunWith(MockitoJUnitRunner.class)
mit@ExtendWith(MockitoExtension.class)
-
@RunWith(SpringRunner.class)
mit@ExtendWith(SpringExtension.class)
-
@Ignore
mit@Disabled
// wobei man diese Annotation eigentlich vermeiden sollte ;)
das "public" kann auch überall weg
Achtung: je nachdem welche Einrückung der Code hat, muss das hier angepasst werden vor dem "Find/Replace"
@Test
public void
ersetzen mit
@Test
void
- beim nächsten Punkt gehe ich bein "Find/Replace" davon aus, dass
@Before
schon mit@BeforeEach
ersetzt wurde
@BeforeEach
public void
ersetzen mit
@BeforeEach
void
Test die Fehler erwarten
- hier muss folgendes mit
assertThrows()
ersetzt werden (Beispiel):
@Test(expected = IllegalArgumentException.class)
public void testConvertToEntityAttributeThrowsExceptionEmptyString() {
uut.convertToEntityAttribute("");
}
ersetzen mit
@Test
public void testConvertToEntityAttributeThrowsExceptionEmptyString() {
assertThrows(IllegalArgumentException.class, () -> uut.convertToEntityAttribute(""));
}
Parametrized Tests
- Diese müssen auch umgebaut werden
-
@RunWith(Parameterized.class)
als Annotation auf der Klasse entfernen - und durch nichts ersetzen -
@Parameters
als Annotation auf der Methode, welche die Parameter liefert, ebenfalls entfernen - und durch nichts ersetzen - Da es nicht so einfach ist hier pauschal irgendwelche Dinge zu ersetzen, hier ein Beispiel Code:
// erwartet wird ein Stream mit Arguments
static Stream<Arguments> getArgs() {
return Stream.of(
Arguments.of(5, 5, 10),
Arguments.of(1, 1, 2)
);
}
@ParameterizedTest(name = "{displayName} - [{index}] {argumentsWithNames}")
@MethodSource("getArgs")
// hier muss dann bei den Methodenparameter die Typisierung erfolgen
void test_addition_withParameters(int val1, int val2, int result) {
int res = val1 + val2; // hier eigentlicher test, methodenaufruf, etc.
assertThat(res).isEqualTo(result);
}
Spring ContextConfiguration
- Bei Spring kann man Tests andere Klassen im Kontext bereitstellen. Für JUnit 5 kann man diese beiden Annotations
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = {SomeConfig.class, SpecialMapper.class})
mit folgender Annotation ersetzen
@SpringJUnitConfig(classes = {SomeConfig.class, SpecialMapper.class})
UnnecessaryStubbingException
Mockito hat als default seit Version 2.x ein "strict stubbing"
- Dies ist eigentlich gedacht, um auf unnötige
when(...).thenReturn(...)
mit einem Fehler hinzuweisen. - Am besten man schaut sich die Tests genauer und strukturiert sie um.
- Wenn man dennoch ein paar Mocks an einer zentralen Stelle behalten will (z.B. im
@BeforeEach
), kann man einfach folgende Annotation auf der Klasse hinzufügen@MockitoSettings(strictness = Strictness.LENIENT)
.
Temporary Test Folder
- Um ein temporäres Verzeichnis zu bekommen einfach folgendes
@Rule
public final TemporaryFolder folder = new TemporaryFolder();
ersetzen mit
@TempDir
Path directory; // darf nicht private sein!
Surefire Plugin
Falls das Projekt bisher kein Surefire Plugin verwendet hat, dann muss man das jetzt mit JUnit 5 auf jeden Fall einbauen.
Dependency Exclusions
Hat man alle Schritte gemacht und die offensichtlichen JUnit 4 bzw. die Vintage Engine Dependency entfernt und man findet doch noch eine JUnit 4 JAR im Classpath, dann muss man sich mittels
mvn dependency:tree -DoutputFile=dep-tree.txt
auf die Suche machen, welche andere Library JUnit 4 als Abhängigkeit mit bringt.
- Das Entfernen der Abhängigkeit kann man dann folgendermaßen erreichen:
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>
Persönliches Fazit
JUnit 5 bringt schon einige Vereinfachungen mit, dass z.B. überall das "public" weg fällt oder, dass es neue "Extensions Modell". Außerdem finde ich, gehört die Unterstützung von Java 8 zum heutigen Zeitpunkt schon zu den Mindestanforderungen (ich weiß, eigentlich kann man nun Java 8 auch schon als "legacy" betrachten). Zusätzlich machen noch so Themen wie Testcontainer und eigene Extensions, etc. die Verwendung von JUnit 5 interessanter. Aus diesen Gründen sollte eine Migration von JUnit 4 auf JUnit 5 spätestens jetzt passieren, falls noch nicht geschehen. Mit IntelliJ hat man ein Tool, welches einem bei den Basics unterstützt. Wenn man sich jedoch ein bisschen mit JUnit 5 auseinandersetzen möchte/sollte, rate ich dazu, die Änderungen mindestens an ein paar Klassen manuell durchzuführen, um ein Gefühl dafür zu bekommen. Bei komplizierteren Konstellationen kommt man um manuelle Änderungen ohnehin nicht herum.