Testen
Tests sind ausführbare Behauptungen über den eigenen Code. Welche Arten es gibt, warum die Testpyramide eine Kostenfrage ist, warum 100 % Abdeckung wenig beweisen – und wie ein Rechner selbst die Testfälle findet, an die man nicht gedacht hat.
Ein automatischer Test ist Code, der anderen Code ausführt und prüft, ob das Ergebnis stimmt. Er dient zweierlei: Er findet Fehler, und er erlaubt Änderungen ohne Angst – wer umbaut und alle Tests bleiben grün, hat wahrscheinlich nichts kaputt gemacht.
Man unterscheidet nach Reichweite: Unit-Tests prüfen eine kleine Einheit isoliert, schnell und zuverlässig. Integrationstests prüfen das Zusammenspiel mit Datenbank, Netzwerk oder anderen Modulen. End-to-End-Tests prüfen das ganze System wie ein Nutzer. Je höher, desto realistischer – aber auch langsamer, teurer und wackeliger. Daraus ergibt sich die Testpyramide: viele kleine Tests unten, wenige große oben.
Die Testpyramide
Die Pyramide ist keine Stilfrage, sondern eine Rechnung: Jeder Test kostet Laufzeit, und jeder hat eine kleine Wahrscheinlichkeit, ohne echten Fehler zu scheitern – weil ein Netzwerk hakt, eine Zeitgrenze knapp ist, ein Browser zu langsam lädt. Stell die Mischung selbst zusammen:
Die umgedrehte Form – wenige Unit-Tests, ein Berg manueller und End-to-End-Tests – heißt spöttisch Eistüte. Sie ist häufig, weil End-to-End-Tests sich am realistischsten anfühlen. Aber eine Testsuite, die eine Stunde läuft und jedes dritte Mal grundlos rot ist, wird nicht mehr ernst genommen. Und ein roter End-to-End-Test sagt nur „irgendwo ist etwas kaputt“; ein roter Unit-Test sagt, wo.
Was einen guten Test ausmacht
Schnell
Was Minuten dauert, läuft man nicht vor jedem Commit. Unit-Tests sollten in Millisekunden fertig sein.
Deterministisch
Gleicher Code, gleiches Ergebnis – jedes Mal. Keine Abhängigkeit von Uhrzeit, Zufall, Reihenfolge oder Netzwerk.
Aussagekräftig
Scheitert er, weiß man, was kaputt ist. Ein Test prüft eine Sache und hat einen Namen, der sie beschreibt.
Ein wackeliger Test (flaky test), den man „einfach noch mal laufen lässt“, ist schlimmer als keiner: Er gewöhnt das Team daran, Rot zu ignorieren. Die häufigsten Ursachen: Thread.sleep statt Warten auf eine Bedingung, Abhängigkeit von der Systemuhr, gemeinsamer Zustand zwischen Tests und nebenläufiger Code mit Race Conditions.
Mocks, Stubs und Fakes
Ein Unit-Test soll eine Einheit isoliert prüfen. Hängt sie von etwas Langsamem oder Unberechenbarem ab – einer Datenbank, einem Zahlungsdienst, der Uhr –, ersetzt man das durch ein Test-Double:
| Art | Was es tut | Beispiel |
|---|---|---|
| Stub | liefert vorbereitete Antworten | „Der Wechselkurs ist immer 1,08.“ |
| Mock | prüft zusätzlich, ob und wie er aufgerufen wurde | „Wurde sendeMail genau einmal mit dieser Adresse aufgerufen?“ |
| Fake | eine echte, aber vereinfachte Implementierung | ein Repository auf Basis einer HashMap statt der Datenbank |
| Echte Abhängigkeit im Container | das Original, für den Test frisch gestartet | PostgreSQL per Testcontainers – für Integrationstests |
Mocks sind mächtig, aber verführerisch. Wer alles mockt, testet am Ende, dass der Code die Methoden aufruft, die er aufruft – und jeder Umbau zerbricht Dutzende Tests, obwohl sich am Verhalten nichts geändert hat. Faustregel: Mocken, was man nicht besitzt oder was langsam und unberechenbar ist. Den Rest echt benutzen oder durch Fakes ersetzen. Und zur Uhrzeit: Code, der Instant.now() direkt aufruft, ist schwer testbar – übergibt man eine Clock, kann der Test die Zeit festlegen.
Abdeckung ist nicht Qualität
Die Testabdeckung misst, welche Zeilen beim Testen ausgeführt wurden. Sie zeigt zuverlässig, was gar nicht getestet ist. Aber ausgeführt heißt nicht geprüft. Besser misst das ein Mutationstest: Ein Werkzeug baut absichtlich kleine Fehler ein – >= wird zu >, ein Rückgabewert zu 0 – und schaut, ob ein Test das bemerkt. Jeder unbemerkte „Mutant“ ist ein Fehler, den die Tests nicht finden würden. Wähle Tests aus:
Property-based Testing
Bei klassischen Tests denkt sich der Entwickler Beispiele aus – und übersieht genau die Fälle, an die er auch beim Programmieren nicht gedacht hat. Property-based Testing dreht das um: Man beschreibt eine Eigenschaft, die immer gelten muss, und das Werkzeug erzeugt Hunderte zufällige Eingaben. Findet es einen Gegenbeispiel, schrumpft es ihn auf den kleinsten Fall, der noch scheitert.
Die Kunst liegt im Finden guter Eigenschaften. Bewährte Muster: Hin und zurück ergibt das Original (serialisieren, deserialisieren). Das Ergebnis erfüllt eine Bedingung (sortiert, keine Duplikate, Summe gleich). Eine einfache, langsame Implementierung liefert dasselbe wie die schnelle. Zweimal anwenden ändert nichts mehr (Idempotenz).
Test-Driven Development
Beim TDD schreibt man den Test vor dem Code, in einem kurzen Kreislauf: Rot – ein Test, der noch scheitert. Grün – gerade so viel Code, dass er besteht. Aufräumen – den Code verbessern, während alle Tests grün bleiben. Der Hauptnutzen ist weniger die Testabdeckung als das Design: Code, der von Anfang an testbar sein muss, bekommt fast zwangsläufig klare Schnittstellen und wenig versteckte Abhängigkeiten. Ob man TDD streng befolgt, ist Geschmackssache; den Kreislauf beim Beheben eines Fehlers zu nutzen – erst ein Test, der den Fehler zeigt, dann die Korrektur – lohnt sich immer.
Für Entwickler übersetzt
// JUnit + AssertJ: ein Test, viele Fälle
@ParameterizedTest
@CsvSource({ "20000, true, 2000", "20000, false, 1000", "9999, true, 0", "10000, true, 1000" })
void rabatt(long betrag, boolean stamm, long erwartet) {
assertThat(Preise.rabattCent(betrag, stamm)).isEqualTo(erwartet);
}
// Mockito: Zahlungsdienst ersetzen, Aufruf prüfen
ZahlungsDienst zahlung = mock(ZahlungsDienst.class);
when(zahlung.belaste(any(), anyLong())).thenReturn(Ergebnis.OK);
new Kasse(zahlung).bezahle(kunde, 5990);
verify(zahlung).belaste(kunde, 5990);
// Testcontainers: echte PostgreSQL für den Integrationstest
@Container
static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:16");
// jqwik: Property-based Testing
@Property
void sortierenBehaeltLaenge(@ForAll List<Integer> xs) {
assertThat(sortiere(xs)).hasSize(xs.size());
}| Werkzeug | Wofür |
|---|---|
| JUnit | das Fundament: Tests schreiben und ausführen, parametrisieren, verschachteln |
| AssertJ | lesbare Prüfungen mit guten Fehlermeldungen |
| Mockito | Stubs und Mocks |
| Testcontainers | echte Datenbanken, Kafka und andere Dienste in Docker – frisch für jeden Testlauf |
| jqwik | Property-based Testing für die JUnit-Plattform |
| PIT | Mutationstests für Java – mvn org.pitest:pitest-maven:mutationCoverage |
| JaCoCo | Testabdeckung messen |
| ArchUnit | Architekturregeln als Test: „Der Controller greift nie direkt auf ein Repository zu.“ |