- 1 Section
- 10 Lessons
- unbegrenzt
- Java Fortgeschritten10
- 1.1OOP in Java: Klassen, Kapselung, Konstruktoren
- 1.2Vererbung in Java
- 1.3Polymorphismus und Methodenüberschreibung
- 1.4Interfaces und abstrakte Klassen
- 1.5Collections: ArrayList, HashMap, LinkedList
- 1.6Generics und Typsicherheit
- 1.7Streams und Lambda-Ausdrücke
- 1.8Dateioperationen: IO und NIO
- 1.9Unit-Tests in Java: JUnit 5
- 1.10Praxisprojekt: Anwendung mit OOP und Tests
Unit-Tests in Java: JUnit 5
Code, der nicht getestet ist, ist nur eine Hoffnung. Unit-Tests sind kleine automatisierte Tests, die einzelne Klassen oder Methoden isoliert prüfen – schnell, deterministisch, oft im Sekundentakt. Bei jedem Build laufen sie und melden sofort, wenn eine Änderung etwas kaputt gemacht hat. Sie sind das Sicherheitsnetz, das Refactoring überhaupt erst ermöglicht.
In Java ist JUnit seit Jahren der Standard – aktuell in Version 5 (auch „JUnit Jupiter" genannt). Diese Lektion zeigt dir die wichtigen Annotations, das AAA-Pattern, Assertions, parametrisierte Tests und wie ein guter Test eigentlich aussieht. Vorwissen: Klassen und Methoden aus L1, Exceptions aus L8.
1) Wozu Unit-Tests?
Manuelles Testen reicht für triviale Programme. Sobald Code aber wächst, hat manuelle Prüfung Grenzen:
- Regressionen – eine Änderung an Stelle X bricht etwas an Stelle Y. Ohne Tests merkt das niemand
- Refactoring – wer wagt es, eine zentrale Klasse umzubauen, wenn jeder Änderung nur per Daumen-Test geprüft wird?
- Edge Cases – Null-Werte, leere Listen, negative Zahlen werden im Alltag oft übersehen
- Dokumentation – ein Test zeigt, wie eine Methode benutzt werden soll
Eine Faustregel: gute Codebasen haben mindestens so viel Testcode wie Produktivcode, oft sogar deutlich mehr.
2) Der einfachste Test
Ein JUnit-Test ist eine ganz normale Methode mit der Annotation @Test. Beispiel: wir haben eine simple Klasse Rechner und schreiben dazu Tests:
public class Rechner { public int addiere(int a, int b) { return a + b; } }
import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; public class RechnerTest { @Test void addiereZweiPositiveZahlen() { Rechner r = new Rechner(); int ergebnis = r.addiere(2, 3); assertEquals(5, ergebnis); } }
Was passiert:
- Die IDE oder das Build-Tool (Maven, Gradle) findet alle
@Test-Methoden automatisch - Jede Test-Methode wird unabhängig ausgeführt
- Schlägt eine Assertion fehl, gilt der Test als fehlgeschlagen
- Wirft die Methode eine Exception, ist der Test ebenfalls fehlgeschlagen
- Läuft sie ohne Probleme durch, ist der Test grün
3) Wichtige Annotations in JUnit 5
static sein.static."Addition gibt korrekte Summe".4) Test-Lifecycle
Die Reihenfolge, in der die Annotation-Methoden laufen:
5) @BeforeEach – Test-Setup
Wenn mehrere Tests dasselbe Objekt brauchen, lagert man die Vorbereitung aus:
public class WarenkorbTest { private Warenkorb wk; @BeforeEach void setUp() { wk = new Warenkorb(); wk.add(new Artikel("Buch", 20.00)); } @Test void leererWarenkorbHatGesamtNull() { wk.clear(); assertEquals(0.00, wk.gesamt(), 0.001); } @Test void einArtikelGibtSeinenPreis() { assertEquals(20.00, wk.gesamt(), 0.001); } }
Vor jedem Test bekommt wk einen frischen Warenkorb mit einem Buch. Kein Test muss sich um die Initialisierung kümmern – einer Methode tut, was sie soll.
6) Das AAA-Pattern
Gute Tests sind nach einem klaren Schema aufgebaut – AAA: Arrange, Act, Assert:
Konkret im Code:
@Test void rabattWirdAufGesamtbetragAngewendet() { // Arrange Warenkorb wk = new Warenkorb(); wk.add(new Artikel("A", 100)); wk.setRabatt(10); // 10 % // Act double total = wk.gesamt(); // Assert assertEquals(90.00, total, 0.001); }
Wenn der Test länger wird als ein paar Zeilen pro Phase – besonders der Act-Teil – ist das ein Zeichen, dass die zu testende Methode zu viel auf einmal tut. Klein und fokussiert.
7) Assertions – das Repertoire
JUnit bietet eine Reihe von Assertions in org.junit.jupiter.api.Assertions:
import static org.junit.jupiter.api.Assertions.*; // Gleichheit assertEquals(erwartet, tatsaechlich); assertEquals(3.14, pi, 0.001); // Toleranz für double // Ungleichheit assertNotEquals(0, ergebnis); // Boolean assertTrue(liste.isEmpty()); assertFalse(liste.contains("X")); // Null-Check assertNull(maybeNull); assertNotNull(objekt); // Referenzgleichheit assertSame(a, b); assertNotSame(a, b); // Arrays/Listen elementweise assertArrayEquals(new int[]{1, 2}, ergebnisArray); assertIterableEquals(List.of("a", "b"), liste); // Explizit scheitern fail("Hier sollte nie hinkommen");
Jede Assertion akzeptiert als letzten Parameter optional eine Fehlermeldung:
assertEquals(5, ergebnis, "Addition von 2+3 sollte 5 ergeben");
8) Exceptions testen
Will man prüfen, dass eine Methode bei falscher Eingabe eine Exception wirft, nutzt man assertThrows:
@Test void divisionDurchNullWirftException() { Rechner r = new Rechner(); ArithmeticException ex = assertThrows( ArithmeticException.class, () -> r.teile(10, 0) ); assertEquals("/ by zero", ex.getMessage()); }
Das Lambda übergibt die zu testende Aktion. assertThrows ruft sie auf und prüft, dass die erwartete Exception fliegt. Das Rückgabe-Exception-Objekt kann weiter inspiziert werden.
9) Parametrisierte Tests
Dieselbe Logik mit verschiedenen Eingaben prüfen, ohne zehn Methoden zu schreiben:
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.junit.jupiter.params.provider.CsvSource; @ParameterizedTest @ValueSource(ints = {2, 4, 6, 8, 100}) void geradeZahlenWerdenAlsGeradeErkannt(int n) { assertTrue(MathUtil.istGerade(n)); } @ParameterizedTest @CsvSource({ "2, 3, 5", "-1, 1, 0", "0, 0, 0", "100, 200, 300" }) void additionFunktioniert(int a, int b, int erwartet) { assertEquals(erwartet, new Rechner().addiere(a, b)); }
JUnit ruft die Methode einmal pro Wert auf. Du siehst dann in der Test-Übersicht eine Liste aller Eingaben – wenn nur eine fehlschlägt, weißt du sofort, welche.
10) Test-Naming – sprechende Namen
Der Test-Name dokumentiert, was geprüft wird. Drei verbreitete Konventionen:
| Stil | Beispiel |
|---|---|
| Beschreibend (Snake-Case-artig) | addiereZweiPositiveZahlen() |
| Given/When/Then | givenLeerenWarenkorb_whenAddiere_thenSizeEins() |
| Behavior | warenkorbZaehltArtikelKorrekt() |
Mit @DisplayName kannst du eine schöne Beschreibung mitgeben, die Reports lesbar macht:
@Test @DisplayName("Addition zweier positiver Zahlen ergibt korrekte Summe") void testAddition() { ... }
11) assertAll – mehrere Checks bündeln
Mehrere zusammengehörige Assertions in einem Test:
@Test void kundeWirdKorrektAngelegt() { Kunde k = new Kunde("Anna", 28, "anna@example.com"); assertAll("Kunden-Daten", () -> assertEquals("Anna", k.getName()), () -> assertEquals(28, k.getAlter()), () -> assertEquals("anna@example.com", k.getMail()) ); }
Vorteil gegenüber drei einzelnen Assertions in Reihe: alle Checks werden geprüft, auch wenn der erste fehlschlägt. Du siehst dann alle Probleme auf einmal, statt sie nacheinander zu beheben.
12) Test-Isolation
Eine der wichtigsten Regeln: Tests müssen unabhängig sein. Drei Konsequenzen:
- Keine geteilten Variablen zwischen Tests: alles per
@BeforeEachfrisch aufbauen - Keine Reihenfolgen-Abhängigkeit: Test B darf nicht voraussetzen, dass Test A vorher lief
- Keine externen Side-Effects: keine echten Datenbanken, keine echten Server-Aufrufe, kein Schreiben in echte Dateien
Wenn ein Test eine Datenbank braucht, nutzt man entweder eine In-Memory-DB (H2), eine eigene Test-DB oder ein Mock. Echte Produktiv-Systeme werden niemals im Test angesprochen.
13) Mocking mit Mockito (Vorschau)
Wenn deine Klasse von anderen abhängt (z. B. einer Datenbank-Klasse), willst du diese Abhängigkeit im Test austauschen – durch ein Mock, das du programmieren kannst. Standard-Bibliothek dafür: Mockito:
import static org.mockito.Mockito.*; @Test void verschickeEmailNutztDenMailService() { // Mock erzeugen MailService mock = mock(MailService.class); // Mock-Verhalten festlegen when(mock.sende(any())).thenReturn(true); Bestellung b = new Bestellung(mock); b.bestaetige("anna@x.de"); // Prüfen, dass die Methode aufgerufen wurde verify(mock).sende(any(String.class)); }
Mockito ersetzt die echte Klasse durch ein steuerbares Test-Double. Wichtig für isolierte Tests, vor allem bei Spring-Anwendungen. Vertiefung in eigenen Tutorials – hier reicht das Grundprinzip.
14) Code Coverage
Wie viel deines Codes ist eigentlich durch Tests abgedeckt? Tools wie JaCoCo messen das – sie zeigen pro Zeile an, ob sie von einem Test mindestens einmal ausgeführt wurde.
Häufige Stufen:
- 0–50 % – kritisches Niveau, viel Code ist ungetestet
- 50–70 % – akzeptabel für viele Projekte
- 70–90 % – professioneller Standard
- > 90 % – sehr gut, aber selten wirtschaftlich. Über 100 % zu jagen ist meist sinnlos
Aber: 100 % Coverage heißt nicht 100 % korrekt. Ein Test, der den Code zwar durchläuft, aber nichts wirklich prüft, gibt grüne Coverage und falsche Sicherheit. Aussagekraft > Quantität.
15) Test-Driven Development (TDD)
Eine beliebte Methodik: erst den Test schreiben, dann den Code, der ihn grün macht. Der typische Zyklus heißt „Red, Green, Refactor":
- Red – Test schreiben, der scheitert (weil der Code noch nicht existiert)
- Green – Minimalen Code schreiben, der den Test grün macht
- Refactor – den Code aufräumen, mit Sicherheit, dass der Test weiter grün bleibt
TDD zwingt zu kleinen, fokussierten Methoden und sorgt fast automatisch für hohe Test-Abdeckung. Nicht für jeden Code geeignet (UI, Prototypen), aber bei klar definierter Logik sehr produktiv.
16) Maven-Setup für JUnit 5
Damit du JUnit 5 nutzen kannst, brauchst du diese Dependency in pom.xml:
<dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.0</version> <scope>test</scope> </dependency>
Maven legt Tests in src/test/java ab – getrennt vom Produktiv-Code in src/main/java. Beim Build (mvn test) werden automatisch alle Tests gefunden und ausgeführt.
17) Häufige Fehler
- Tests im Produktiv-Pfad: Test-Klassen gehören nach
src/test/java, nicht ins gleiche Verzeichnis wie der echte Code - Test prüft, dass kein Fehler fliegt – aber sonst nichts: ein
@Test void doSomething() { obj.tueWas(); }ohne Assertion ist wertlos - Mehrere Methoden in einem Test: Ein Test sollte eine Sache prüfen. „Test für Methode X" mit fünf verschiedenen Szenarien gehört in fünf Tests
- Reihenfolgen-Abhängigkeit: Test B nimmt an, dass A schon gelaufen ist. Verboten. JUnit-Reihenfolge ist nicht garantiert
- Echte Datenbank/Netzwerk: macht Tests langsam und unzuverlässig. In-Memory oder Mock
- Test ignoriert Failure:
try { ... } catch (Exception e) {}im Test verschluckt Probleme - Hartcodierte Werte überall: lieber Konstanten oder Konstruktor-Args mit klarem Namen
- Tests, die zu viel mocken: wenn du fast nur Mocks testest, prüfst du dein Test-Setup, nicht deinen Code
@Test, @BeforeEach) gibt es. JUnit-spezifische Details und Mockito sind selten Stoff.
Zusammenfassung
Unit-Tests sind automatisierte Prüfungen einzelner Code-Einheiten. In Java ist JUnit 5 der Standard. Tests werden mit @Test markiert; @BeforeEach baut das Test-Objekt frisch auf, @AfterEach räumt auf. Das AAA-Pattern (Arrange/Act/Assert) strukturiert den Test. Assertions wie assertEquals, assertTrue, assertNull, assertThrows prüfen das erwartete Ergebnis. Mit parametrisierten Tests (@ParameterizedTest + @ValueSource/@CsvSource) lassen sich viele Eingaben kompakt prüfen. Tests müssen unabhängig sein – keine geteilten Variablen, keine Reihenfolgen-Abhängigkeit, keine echten externen Systeme (stattdessen Mocks mit Mockito). Code Coverage (z. B. JaCoCo) zeigt, wie viel Code getestet ist – aber Qualität schlägt Quote. TDD (Red-Green-Refactor) ist ein verbreiteter Workflow.
