Java JUnit Einführung
Was JUnit ist, wie man es zu einem Java-Projekt hinzufügt und wie man den ersten JUnit-Test schreibt.
JUnit ist das De-facto-Standard-Framework zum Schreiben automatisierter Tests in Java. Ein Test ist eine kleine Methode, die einen Teil deines Codes ausführt und behauptet (asserts), dass er sich wie erwartet verhält; JUnit findet diese Methoden, führt jede einzeln aus, überprüft die Assertions und meldet, welche bestanden und welche fehlgeschlagen sind. Die aktuelle Generation, JUnit 5 (auch JUnit Jupiter genannt), wird als eine Reihe kleiner Bibliotheken ausgeliefert, die du deinem Build hinzufügst — sie ist nicht Teil des JDK — und sie treibt den mvn test / gradle test-Schritt an, der fast jede Java-Projekt-CI absichert.
Wozu überhaupt ein Test-Framework?
Du könntest Code manuell mit main-Methoden und println überprüfen — aber das skaliert schlecht. Ein Framework gibt dir vier Dinge, die du sonst selbst nachbauen müsstest:
- Discovery — es findet automatisch jede
@Test-Methode; du musst nie eine Liste pflegen. - Isolation — jeder Test bekommt eine frische Fixture, sodass ein Test keinen anderen korrumpieren kann.
- Assertions — ein umfangreiches Vokabular (
assertEquals,assertThrows, …), das präzise Fehlermeldungen erzeugt. - Reporting — eine einheitliche Pass/Fail-Zusammenfassung, die Build-Tool und IDE verstehen.
Wenn man das einmal richtig umsetzt, werden Tests günstig zu schreiben, was der entscheidende Punkt ist: günstige Tests werden geschrieben, und Code, der getestet wird, kann ohne Angst geändert werden.
JUnit zu einem Projekt hinzufügen
JUnit 5 ist eine Abhängigkeit, die in der Build-Datei deklariert wird. Mit Maven zieht der junit-jupiter-Aggregator die API (zum Kompilieren) und die Engine (zum Ausführen) herein:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>Mit Gradle sind es zwei Zeilen plus der useJUnitPlatform()-Schalter:
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.3'
}
test {
useJUnitPlatform()
}Test-Quellcodes liegen unter src/test/java, gespiegelt nach dem Paket der Klasse, die sie testen. Der test-Scope hält JUnit aus deinem Produktionsartefakt heraus.
Dein erster Test
Ein JUnit-Test ist eine gewöhnliche Methode, die mit @Test annotiert ist. Darin rufst du den zu testenden Code auf und machst Assertions über das Ergebnis. Hier ist ein Calculator und eine Test-Klasse dafür:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class CalculatorTest {
private final Calculator calc = new Calculator();
@Test
void addReturnsSum() {
assertEquals(5, calc.add(2, 3));
}
@Test
void divideByZeroThrows() {
assertThrows(ArithmeticException.class, () -> calc.divide(1, 0));
}
}Beachte das Muster, dem jeder Test folgt — Arrange (eine Fixture aufbauen), Act (die Methode aufrufen), Assert (das Ergebnis prüfen). Test-Methoden sind paketprivat (kein public nötig in JUnit 5) und geben void zurück. Führe mvn test aus, und der Build wird nur grün, wenn jede Assertion gilt.
Die Annotationen und Assertions, die du am häufigsten verwenden wirst
JUnit hat eine kleine Oberfläche. Diese wenigen Member decken die große Mehrheit realer Tests ab:
| Member | Paket / Klasse | Zweck |
|---|---|---|
@Test | org.junit.jupiter.api | Markiert eine Methode als Test |
@BeforeEach / @AfterEach | org.junit.jupiter.api | Wird vor/nach jedem Test ausgeführt (Fixture aufbauen / abbauen) |
@BeforeAll / @AfterAll | org.junit.jupiter.api | Wird einmal vor/nach allen Tests in der Klasse ausgeführt |
@DisplayName | org.junit.jupiter.api | Ein für Menschen lesbarer Name für Berichte |
@Disabled | org.junit.jupiter.api | Einen Test vorübergehend überspringen |
assertEquals(exp, act) | Assertions | Schlägt fehl, wenn die beiden nicht gleich sind |
assertTrue / assertFalse | Assertions | Schlägt fehl, wenn ein boolean nicht zutrifft |
assertThrows(type, exec) | Assertions | Schlägt fehl, wenn das Lambda diese Ausnahme nicht wirft |
assertNull / assertNotNull | Assertions | Schlägt fehl bei falscher Nullness |
@BeforeEach gibt jedem Test einen sauberen Ausgangszustand — JUnit erstellt für jeden @Test eine frische Instanz der Test-Klasse und führt dann das Setup aus, sodass Zustand nie zwischen Tests durchsickert.
Was JUnit für dich tut, in einer ausführbaren Datei
Der Code-Runner hier hat kein JUnit im Classpath (es ist eine externe Bibliothek, kein Teil des JDK), daher reimplementiert das folgende Beispiel JUnits Kern-Loop in reinem Java: eine Fixture, die vor jedem Test neu erstellt wird, eine kleine Menge von assertXxx-Hilfsmethoden, eine Liste von Test-Methoden, die unabhängig ausgeführt werden, und eine Pass/Fail-Bilanz am Ende. Das ist genau die Maschinerie, die JUnit automatisiert — sie unverhüllt zu sehen, macht das echte Framework offensichtlich. Ein Test schlägt absichtlich fehl, damit du sehen kannst, wie Rot aussieht.
Was man der Ausführung entnehmen kann:
- Die drei gesunden Tests drucken
PASSund der vierte drucktFAIL deliberatelyFailing -> expected <10> but was <5>— eine präzise Fehlermeldung, nicht nur "ein Test ist fehlgeschlagen". Diese Differenz (expected … but was …) ist genau das, was JUnitsassertEqualsliefert, und was einen roten Test auf einen Blick diagnostizierbar macht. setUp()wird vor jedem Test ausgeführt, sodasscalcjedes Mal ein frischerCalculatorist. Das ist der@BeforeEach-Vertrag: Tests sind isoliert, und die Reihenfolge, in der sie ausgeführt werden, kann nie eine Rolle spielen, weil keiner von ihnen veränderlichen Zustand teilt.divideThrowsOnZerobesteht, indem bestätigt wird, dass eine Ausnahme geworfen wird —assertThrowsmacht "dies sollte fehlschlagen" zu einer erstklassigen, positiven Assertion anstatt eines fragilen try/catch. Erwartete Ausnahmen sind Verhalten, das es wert ist, getestet zu werden, keine Fehler zum Schlucken.- Die abschließende Bilanz —
Tests run: 4, Passed: 3, Failed: 1, Assertions: 5— ist der Bericht. Ein fehlgeschlagener Test von vier setzt den gesamten Build aufRED; CI behandelt jeden Fehler als Stopp, weshalb eine grüne Suite bedeutungsvoll ist. - Hier wurde nichts von JUnit importiert, doch die Form ist identisch: Entdeckung annotierter Methoden, Setup pro Test, Assertions, Zusammenfassung. JUnits Wert besteht darin, diese Schleife zu automatisieren (und Discovery, Parallelismus, parametrisierte Tests und IDE-Integration hinzuzufügen), sodass du nur die Test-Bodies schreibst.
Was der Rest dieses Teils behandelt
Dieser Teil baut auf dem Kern-Loop auf, den du gerade gesehen hast:
- JUnit-Annotationen —
@Test,@DisplayName,@Disabledund der Rest der Marker-Menge. - Der Test-Lifecycle — wie
@BeforeEach/@AfterEach/@BeforeAll/@AfterAlljedem Test eine saubere Fixture geben. - Assertions — der vollständige
assertXxx-Katalog, einschließlichassertThrowsfür das Testen von Ausnahmen. - Parametrisierte Tests — einen Test-Body über viele Eingaben ausführen, anstatt Fälle zu kopieren.
- Mocking mit Mockito — die Kollaboratoren einer Klasse durch Platzhalter ersetzen, damit ein Unit-Test ein Unit-Test bleibt.
Wenn du das größere Bild verstehen möchtest, warum automatisiertes Testen wichtig ist, bevor du in die API eintauchst, sieh dir Testing in Java an. Andernfalls beginnt das nächste Kapitel dort, wo jede Suite beginnt: eine Test-Klasse zu definieren und auszuführen.