Einführung in das Testen mit Java
Warum Tests in Java wichtig sind, die Testing-Pyramide und ein Überblick über gängige Java-Test-Frameworks.
Automatisierte Tests beweisen, dass Code das tut, was man erwartet – und belegen das auch dann noch, wenn sich der Code verändert. Anstatt das Programm manuell auszuführen und die Ausgabe zu prüfen, schreibt man kleine Programme, die den eigenen Code ausführen und die Ergebnisse automatisch überprüfen. In Java ist dieses Ökosystem um Frameworks wie JUnit und Mockito herum aufgebaut, aber die zugrunde liegenden Ideen – Arrange, Act, Assert – sind einfach genug, um sie von Hand umzusetzen. Dieses Kapitel gibt einen Überblick, bevor die späteren Kapitel auf jedes Werkzeug im Detail eingehen.
Warum automatisierte Tests wichtig sind
Ein Test ist eine kleine, wiederholbare Prüfung, ob sich ein Stück Code korrekt verhält. Der Vorteil liegt nicht im ersten Durchlauf – sondern in jedem weiteren danach. Sobald ein Verhalten in einem Test festgehalten ist, schlägt jede Änderung, die es bricht, sofort und lautstark fehl, anstatt erst Wochen später als Bug in der Produktion aufzutauchen. Tests dokumentieren auch die Absicht: Ein gut benannter Test beschreibt, was der Code tun soll.
// A test names a behavior, runs the code, and asserts the outcome.
@Test
void addsTwoPositiveNumbers() {
int result = Calculator.add(2, 3);
assertEquals(5, result); // fails the build if result != 5
}Das Ziel ist schnelles Feedback. Eine grüne Test-Suite bedeutet, dass man sicher refaktorieren kann; eine rote zeigt direkt, was kaputt ist.
Die Testing-Pyramide
Tests werden in Schichten eingeteilt, üblicherweise als Pyramide dargestellt. Unit-Tests bilden die Basis: viele davon, schnell, jeder prüft eine Klasse oder Methode isoliert. Integrationstests befinden sich in der Mitte: weniger, langsamer, sie prüfen, ob Komponenten zusammenarbeiten (zum Beispiel der eigene Code zusammen mit einer Datenbank). End-to-End (E2E)-Tests sitzen an der Spitze: wenige, am langsamsten, sie steuern die gesamte Anwendung so, wie ein Nutzer es tun würde.
| Ebene | Umfang | Geschwindigkeit | Anzahl | Java-Werkzeuge |
|---|---|---|---|---|
| Unit | eine Klasse/Methode | schnell (ms) | viele | JUnit, AssertJ |
| Integration | mehrere Komponenten | mittel | einige | JUnit, Testcontainers |
| End-to-End | gesamtes System | langsam | wenige | Selenium, REST-assured |
Die Form ist entscheidend: Man stützt sich auf günstige, schnelle Unit-Tests für den Großteil der Abdeckung und reserviert die langsamen, anfälligen E2E-Tests für einige wenige kritische Nutzer-Workflows.
Das Arrange–Act–Assert-Muster
Fast jeder Test, in jedem Framework, folgt derselben dreistufigen Struktur. Arrange bereitet Eingaben und Abhängigkeiten vor. Act ruft den zu testenden Code auf. Assert prüft, ob das Ergebnis dem Erwarteten entspricht. Diese Schritte visuell getrennt zu halten, macht einen Test leicht lesbar und leicht zu debuggen, wenn er fehlschlägt.
@Test
void rejectsBlankUsername() {
// Arrange
UserService service = new UserService();
// Act
boolean valid = service.isValidUsername(" ");
// Assert
assertFalse(valid);
}Eine fehlgeschlagene Assertion wirft eine Ausnahme, das Framework zeichnet sie auf, und der Durchlauf geht zum nächsten Test weiter – sodass ein fehlerhaftes Verhalten die anderen nie verbirgt.
JUnit, der Standard-Runner
JUnit ist das De-facto-Framework für Unit-Tests in Java. Man annotiert Methoden mit @Test, JUnit erkennt sie per Reflection, führt jede aus und meldet Erfolg oder Misserfolg. Assertions wie assertEquals, assertTrue und assertThrows sind statische Hilfsmethoden, die den Test fehlschlagen lassen, wenn die Erwartung nicht erfüllt wird. Echte Projekte führen JUnit über ein Build-Werkzeug aus (das Surefire-Plugin von Maven oder Gradles test-Task), nicht von Hand.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class CalculatorTest {
@Test
void dividesNumbers() {
assertEquals(4, Calculator.divide(8, 2));
}
@Test
void throwsOnDivideByZero() {
assertThrows(ArithmeticException.class, () -> Calculator.divide(1, 0));
}
}Da auf diesem Runner keine JUnit-JAR oder kein Build-Werkzeug vorhanden ist, baut das folgende Beispiel dieselbe Idee von Grund auf – ein winziges Testgerüst, das benannte Prüfungen ausführt und Erfolge und Misserfolge zählt, genau das, was @Test und assertEquals intern tun.
Was man aus dem Durchlauf mitnehmen kann:
- Jeder
assertEquals-Aufruf ist ein Testfall – Eingaben vorbereiten (Arrange), durch Aufrufen vonaddoderisBlankhandeln (Act) und das Ergebnis prüfen (Assert) – was genau dem entspricht, was eine JUnit-@Test-Methode tut. - Eine bestandene Prüfung gibt
PASSaus, eine fehlgeschlagene gibtFAILmit dem erwarteten und dem tatsächlichen Wert aus – das ist die Diagnoseinformation, die JUnit-Assertion-Meldungen liefern. - Der absichtlich falsche Fall (
expected 10 but got 5) zeigt, wie ein roter Test aussieht: Das Testgerüst führt die verbleibenden Prüfungen weiter aus, anstatt beim ersten Fehler zu stoppen. - Die Zusammenfassung zählt 5 gesamt, 4 bestanden, 1 fehlgeschlagen – denselben Erfolgs-/Misserfolgs-Bericht, den ein Test-Runner am Ende eines Durchlaufs ausgibt.
- Da ein Test fehlgeschlagen ist, endet das Programm mit
BUILD FAILURE, was zeigt, warum ein einzelner fehlerhafter Test den gesamten Build in CI zum Scheitern bringen sollte.
Wie die Teile zusammenpassen
Javas Test-Werkzeuge bauen aufeinander auf, von einfachen Assertions bis hin zur vollständigen Build-Integration:
- Assertions (
assertEquals,assertThrows) legen fest, was wahr sein muss. - JUnit erkennt und führt
@Test-Methoden aus und meldet die Ergebnisse. - Mockito stellt gefälschte Mitarbeiter bereit, damit eine Unit isoliert getestet werden kann.
- Maven oder Gradle bindet die Test-Suite in den Build ein und lässt den Build bei jedem roten Test fehlschlagen.
- CI führt den Build bei jedem Push aus, damit fehlerhafter Code nie den Hauptzweig erreicht.
Jedes spätere Kapitel behandelt eine Stufe dieser Leiter – zunächst JUnit-Annotationen und Assertions, dann Mocking mit Mockito, dann die Einbindung von Tests in Maven und Gradle. Zu verstehen, wo jedes Werkzeug seinen Platz hat, macht die gesamte Testing-Geschichte zusammenhängend.