Grundlagen

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.

Lesestand

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:

Eine Teststrategie zusammenstellen
Unit
Integration
End-to-End

Modellannahmen: Unit 5 ms und 1 : 100 000 Fehlalarm, Integration 0,5 s und 1 : 2 000, End-to-End 20 s und 1 : 200. Echte Werte schwanken stark – die Größenordnungen nicht.

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:

ArtWas es tutBeispiel
Stubliefert vorbereitete Antworten„Der Wechselkurs ist immer 1,08.“
Mockprüft zusätzlich, ob und wie er aufgerufen wurde„Wurde sendeMail genau einmal mit dieser Adresse aufgerufen?“
Fakeeine echte, aber vereinfachte Implementierungein Repository auf Basis einer HashMap statt der Datenbank
Echte Abhängigkeit im Containerdas Original, für den Test frisch gestartetPostgreSQL 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:

Abdeckung gegen Mutationen

Code

Tests

Eingebaute Fehler (Mutanten)

Die Mutanten werden wirklich ausgeführt: Jeder ist eine leicht veränderte Version der Funktion, gegen die alle gewählten Tests laufen.

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.

Eine Sortierfunktion mit verstecktem Fehler
// Jemand hat „optimiert“: List<Integer> sortiere(List<Integer> xs) { return new ArrayList<>(new TreeSet<>(xs)); // sortiert ja auch … }
Noch nicht gestartet.

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());
}
WerkzeugWofür
JUnitdas Fundament: Tests schreiben und ausführen, parametrisieren, verschachteln
AssertJlesbare Prüfungen mit guten Fehlermeldungen
MockitoStubs und Mocks
Testcontainersechte Datenbanken, Kafka und andere Dienste in Docker – frisch für jeden Testlauf
jqwikProperty-based Testing für die JUnit-Plattform
PITMutationstests für Java – mvn org.pitest:pitest-maven:mutationCoverage
JaCoCoTestabdeckung messen
ArchUnitArchitekturregeln als Test: „Der Controller greift nie direkt auf ein Repository zu.“

Häufige Irrtümer

„100 % Testabdeckung heißt fehlerfrei.“ – Sie heißt, dass jede Zeile einmal ausgeführt wurde. Ob dabei etwas geprüft wurde, zeigt erst ein Mutationstest.
„Tests kosten Zeit.“ – Sie kosten Zeit beim Schreiben und sparen sie bei jeder späteren Änderung. Ohne Tests wird jede Änderung an altem Code ein Risiko, und das Team traut sich irgendwann nicht mehr.
„Ein wackeliger Test wird einfach neu gestartet.“ – Er zeigt fast immer ein echtes Problem: im Test oder – unangenehmer – im Code, etwa eine Race Condition.
„Private Methoden muss man auch testen.“ – Getestet wird das Verhalten über die öffentliche Schnittstelle. Wer das Bedürfnis hat, eine private Methode einzeln zu testen, hat oft eine eigene Klasse gefunden, die herausgelöst werden möchte.
„Je mehr Mocks, desto besser isoliert.“ – Zu viele Mocks testen die Implementierung statt des Verhaltens. Solche Tests zerbrechen bei jedem Umbau und finden trotzdem kaum Fehler.

Verwandte Begriffe