Eigenschaften guter Unit‑Tests
Dieser Beitrag ist eine Erklärung zu Eigenschaften guter Unit‑Tests – inklusive Prüfungsfragen, Kernkomponenten und Tags.
Gute Unit‑Tests sind der Sicherheitsgurt jeder Software. Sie helfen Dir, schnell zu erkennen, ob eine Änderung etwas kaputt gemacht hat, und geben Dir das Vertrauen, Code umzubauen, ohne Angst vor Regressionsfehlern zu haben. Gerade in Prüfungen und im Berufsalltag wird erwartet, dass Du nicht nur Tests schreiben, sondern auch bewerten kannst, was einen guten Unit‑Test ausmacht.
In a Nutshell
Gute Unit‑Tests sind korrekt, isoliert, schnell, aussagekräftig, wartbar und einfach durchführbar. Sie geben schnelles, verlässliches Feedback, lokalisieren Fehler präzise und ermöglichen sicheres Refactoring.
Kompakte Fachbeschreibung
Unit‑Tests prüfen die kleinste testbare Einheit eines Programms, meist eine einzelne Methode oder Klasse. Damit sie ihren Zweck erfüllen, müssen sie mehrere Eigenschaften erfüllen:
1. Korrekt
Ein Test muss genau das Verhalten prüfen, das er dokumentieren soll. Falsche Annahmen oder ungenaue Erwartungen machen einen Test wertlos. Ein korrekter Test ist gleichzeitig eine Spezifikation für das Verhalten der Einheit.
2. Isoliert
Ein Unit‑Test sollte keine externen Systeme wie Datenbanken, Netzwerke, Dateisysteme oder APIs benötigen. Solche Abhängigkeiten ersetzt Du durch Testdoubles wie Stubs, Mocks, Fakes oder Spys. Isolierte Tests laufen stabil und schnell.
3. Schnell
Ein guter Unit‑Test läuft im Millisekunden‑Bereich. Das ermöglicht es, Hunderte oder Tausende Tests mehrmals am Tag auszuführen, ohne dass der Entwicklungsfluss unterbrochen wird. Langsame Tests werden schnell ignoriert oder aus der CI-Pipeline entfernt.
4. Aussagekräftig
Der Testname, die Teststruktur und die Fehlermeldung müssen auf den ersten Blick verständlich sein. Eine klare AAA‑Struktur und präzise Assertions helfen, bei einem Fehlschlag sofort zu verstehen, was schief gelaufen ist.
5. Wartbar
Tests sollten genau so sauber strukturiert sein wie Produktionscode. Kein duplizierter Code, keine unnötige Komplexität, gemeinsame Hilfsbibliotheken und klare Abhängigkeiten machen Tests langfristig pflegeleicht. Gute Tests überstehen Refactorings ohne Änderungen.
6. Einfach durchführbar
Tests müssen mit einem einzigen Befehl startbar sein. Sie dürfen keine manuelle Vorbereitung, keine spezielle Umgebung und keine zeitliche Abhängigkeit benötigen. Ihr Ergebnis muss deterministisch sein: gleiche Eingabe, gleiche Ausgabe.
Prüfungsrelevante Stichpunkte
- AAA (Arrange, Act, Assert): Jeder Unit‑Test besteht aus drei Teilen: Vorbereitung der Daten, Ausführung der zu testenden Methode und Prüfung des Ergebnisses.
- Testdoubles: Stub liefert vordefinierte Antworten, Mock prüft Interaktionen, Fake ist eine vereinfachte Implementierung, Spy protokolliert Aufrufe.
- Naming: Ein Testname wie
methodName_condition_expectedResultmacht das erwartete Verhalten direkt lesbar. - Edge Cases und Fehlerfälle: Nicht nur der Happy Path, sondern auch Nullwerte, leere Eingaben, Grenzwerte und Exceptions gehören getestet.
- Keine „magischen“ Zahlen: Konstanten und Variablen mit sprechenden Namen machen Tests verständlicher und wartbarer.
- Aussagekräftige Assertions: Assertions sollten eine klare Fehlermeldung liefern, zum Beispiel
assertEquals(expected, actual, "Neukunde soll keinen Rabatt erhalten"). - Unabhängigkeit: Tests dürfen nicht voneinander abhängen. Die Reihenfolge sollte egal sein.
- Determinismus: Ein Test muss bei jedem Lauf dasselbe Ergebnis liefern. Zufall, Uhrzeit oder externe Zustände haben in Unit‑Tests nichts zu suchen.
- Ein Test, eine Verantwortung: Jeder Test sollte genau ein Verhalten prüfen. Mehrere Assertions sind erlaubt, sollten aber zusammengehören.
- CI/CD‑Integration: Unit‑Tests sollten automatisch in der Build‑Pipeline laufen und bei Fehlschlag den Build stoppen.
Kernkomponenten
-
Teststruktur (AAA) AAA steht für Arrange, Act, Assert. In der Arrange‑Phase bereitest Du alle notwendigen Objekte und Testdaten vor. In der Act‑Phase rufst Du die Methode auf, die Du testen möchtest. In der Assert‑Phase prüfst Du das Ergebnis.
-
Testdoubles (Stub, Mock, Fake, Spy) Testdoubles ersetzen echte Abhängigkeiten. Ein Stub liefert feste Antworten, ein Mock prüft, ob bestimmte Methoden aufgerufen wurden, ein Fake ist eine vereinfachte Implementierung und ein Spy protokolliert Aufrufe.
-
Naming‑Konventionen Ein guter Testname beschreibt das Szenario und das erwartete Ergebnis. Muster wie
methodName_condition_expectedResultmachen Tests zu lesbarer Dokumentation. -
Assertions Assertions sind die Prüfungen am Ende eines Tests. Sie sollten präzise sein und bei einem Fehlschlag eine aussagekräftige Meldung liefern.
-
Testdaten (Factories, Builder) Testdaten sollten einfach erzeugbar und wiederholbar sein. Factories und Builder helfen, komplexe Objekte aufzubauen, ohne den Test mit Boilerplate zu überladen.
-
Isolation Ein Unit‑Test prüft genau eine Einheit. Datenbanken, Netzwerke, Dateien oder andere Services werden durch Testdoubles ersetzt.
-
Geschwindigkeit Unit‑Tests müssen im Millisekunden‑Bereich laufen. Nur so bleibt die Test‑Suite bei Tausenden Tests ausführbar und gibt schnelles Feedback.
-
Unabhängigkeit Tests dürfen nicht aufeinander aufbauen. Jeder Test erzeugt seine eigenen Daten und hinterlässt keine Zustände, die den nächsten Test beeinflussen.
-
Determinismus Ein Test liefert bei jedem Durchlauf dasselbe Ergebnis. Zufall, Zeit, Netzwerk oder globale Zustände werden durch kontrollierte Eingaben und Testdoubles ersetzt.
-
Wartbarkeit Tests sind Code. Sie profitieren von DRY, klaren Namen, kleinen Methoden und wenig Kopplung. Wartbare Tests überstehen Refactorings und bleiben verständlich.
Praxisbeispiel (Rabattberechnung)
Das folgende Beispiel zeigt einen typischen Unit‑Test in Java mit JUnit. Es geht um eine Methode berechneRabatt, die einen Rabatt für einen Kunden und einen Artikel berechnet. Der Test prüft den Fall, dass ein Neukunde für einen Standardartikel keinen Rabatt erhält.
Was wird hier gezeigt?
- Klare Namensgebung: Der Testname beschreibt Methode, Bedingung und erwartetes Ergebnis.
- AAA‑Struktur: Die Abschnitte Arrange, Act und Assert sind kommentiert und klar getrennt.
- Fokus: Es wird nur eine einzelne Verhaltensweise geprüft.
- Aussagekräftige Assertion: Die Fehlermeldung erklärt, warum der Test wichtig ist.
// Naming: berechneRabatt_neukunde_standardartikel_erwartet0Prozent
@Test
public void berechneRabatt_neukunde_standardartikel_erwartet0Prozent() {
// Arrange
Kunde kunde = new Kunde(Kundentyp.NEUKUNDE);
Artikel artikel = new Artikel(Artikeltyp.STANDARD);
RabattService service = new RabattService();
// Act
int rabatt = service.berechneRabatt(kunde, artikel);
// Assert
assertEquals(0, rabatt, "Neukunde soll keinen Rabatt erhalten");
}
Erklärung: Der Test ist isoliert, weil er keine externen Systeme nutzt. Er ist schnell, weil nur einfache Objekte erzeugt werden. Er ist aussagekräftig, weil Name und Fehlermeldung sofort verständlich sind. Wenn der Test fehlschlägt, weißt Du sofort, dass die Rabattberechnung für Neukunden falsch ist.
Vorteile und Nachteile
Vorteile
- Schnelles Feedback: Unit‑Tests laufen in Millisekunden und geben sofort Auskunft, ob eine Änderung funktioniert.
- Hohe Fehlerlokalisierung: Ein fehlgeschlagener Test zeigt direkt die betroffene Einheit, nicht erst ein großes System.
- Sicheres Refactoring: Mit einer guten Test‑Suite kannst Du Code umgestalten, ohne Angst vor Regressionsfehlern zu haben.
- Geringe Wartungskosten bei guter Struktur: Saubere Tests sind leicht zu verstehen und anzupassen, wenn sich Anforderungen ändern.
- Lebende Dokumentation: Tests beschreiben das Verhalten der Software und helfen neuen Teammitgliedern beim Einstieg.
- Frühe Fehlererkennung: Fehler werden entdeckt, bevor sie in die Produktion oder in höhere Teststufen gelangen.
Nachteile
- Initialer Aufwand: Das Schreiben von Testdoubles und guter Testdaten kostet am Anfang Zeit.
- Gefahr von Over‑Engineering: Zu viele Hilfsbibliotheken, komplexe Factory‑Methoden oder verschachtelte Mocks können die Tests schwerer pflegen als den Produktionscode.
- Falsche Sicherheit: Ein Test, der nicht das Richtige prüft, kann grüne Ergebnisse vortäuschen, während der Code trotzdem fehlerhaft ist.
- Wartung der Tests selbst: Bei schlechter Struktur führt jede kleine Änderung zu vielen angepassten Tests, was den Nutzen zunichtemacht.
- Kein Ersatz für Integrationstests: Unit‑Tests prüfen Einzelteile. Sie ersetzen nicht Tests, die das Zusammenspiel mehrerer Komponenten prüfen.
FAQ: Eigenschaften guter Unit‑Tests
1. Was ist ein Unit‑Test?
2. Was bedeutet AAA in Unit‑Tests?
3. Was macht einen Unit‑Test korrekt?
4. Was bedeutet Isolation bei Unit‑Tests?
5. Was ist ein Testdouble?
6. Was ist der Unterschied zwischen Stub und Mock?
7. Was ist ein Fake?
8. Was ist ein Spy?
9. Warum sollten Unit‑Tests schnell sein?
10. Was bedeutet aussagekräftig in Bezug auf Tests?
11. Was ist eine gute Naming‑Konvention für Unit‑Tests?
methodName_condition_expectedResult, zum Beispiel berechneRabatt_neukunde_standardartikel_erwartet0Prozent. Alternativ eignet sich sollte_X_wenn_Y.12. Was ist ein Assertion?
assertEquals, assertTrue oder assertThrows.13. Was sind magische Zahlen in Tests?
14. Warum sollten Tests unabhängig voneinander sein?
15. Was bedeutet deterministisch bei Unit‑Tests?
16. Was versteht man unter Wartbarkeit von Tests?
17. Was ist der Unterschied zwischen Unit‑, Integrations‑ und E2E‑Tests?
18. Was sind Edge Cases?
19. Was ist ein Happy Path?
20. Was ist eine Test‑Factory?
21. Was ist Refactoring‑Sicherheit bei Tests?
22. Was ist ein flaky Test?
23. Was ist ein Code‑Coverage?
24. Warum sollte ein Test nur eine Verantwortung haben?
25. Was bedeutet einfach durchführbar bei Unit‑Tests?
Lernstrategie
1. Verständniseinstieg: Schreibe Deinen ersten Unit‑Test mit AAA
Nimm eine einfache Methode, zum Beispiel eine Methode, die den Rabatt für einen Kunden berechnet. Schreibe einen Test, der die AAA‑Struktur nutzt und den Testnamen nach dem Muster methodName_condition_expectedResult aufbaut.
Aufgabe: Schreibe einen Test für eine Methode istVolljaehrig(int alter), die true zurückgeben soll, wenn das Alter mindestens 18 ist.
@Test
public void istVolljaehrig_alter18_erwartetTrue() {
// Arrange
int alter = 18;
// Act
boolean ergebnis = pruefer.istVolljaehrig(alter);
// Assert
assertTrue(ergebnis, "Eine Person mit Alter 18 sollte volljährig sein");
}
Lösung: Der Test ist korrekt, isoliert und aussagekräftig. Er prüft einen Grenzfall und nutzt eine klare Assertion mit Fehlermeldung.
2. Vertiefung: Testdoubles einsetzen
Stell Dir vor, Du hast eine Klasse BestellService, die auf eine echte Datenbank zugreift. Schreibe einen Test, in dem Du die Datenbank durch einen Stub ersetzt.
Aufgabe: Ein BestellService soll die Gesamtsumme einer Bestellung berechnen. Er nutzt dafür ein PreisRepository. Schreibe einen Test, der das Repository stubbt.
@Test
public void berechneSumme_einArtikel_preis10_erwartet10() {
// Arrange
PreisRepository stubRepository = new PreisRepository() {
@Override
public double findePreis(String artikelId) {
return 10.0;
}
};
BestellService service = new BestellService(stubRepository);
List<String> artikel = List.of("A1");
// Act
double summe = service.berechneSumme(artikel);
// Assert
assertEquals(10.0, summe, 0.001);
}
Lösung: Der Test isoliert den Service von der Datenbank. Der Stub liefert immer den Preis 10.0, sodass der Test schnell und deterministisch bleibt.
3. Prüfungsfokus: Testdouble‑Arten zuordnen
Übe das Zuordnen von Testdoubles zu Szenarien. Hier ist eine kleine Übung:
| Szenario | Passendes Testdouble |
|---|---|
| Du brauchst eine vordefinierte Antwort für eine Abhängigkeit. | Stub |
| Du möchtest prüfen, ob eine bestimmte Methode aufgerufen wurde. | Mock |
| Du ersetzt eine Datenbank durch eine In‑Memory‑Implementierung. | Fake |
| Du möchtest später überprüfen, welche Aufrufe stattfanden. | Spy |
4. Refactoring: Schlechte Tests verbessern
Nimm den folgenden schlechten Test und forme ihn in einen guten, wartbaren Test um.
Schlechte Version:
@Test
public void test1() {
int x = 5;
int y = 10;
assertEquals(new Calc().add(x, y), 15);
}
Verbesserte Version:
@Test
public void add_zweiPositiveZahlen_erwartetSumme() {
// Arrange
int summand1 = 5;
int summand2 = 10;
Calculator calculator = new Calculator();
// Act
int ergebnis = calculator.add(summand1, summand2);
// Assert
assertEquals(15, ergebnis, "5 + 10 sollte 15 ergeben");
}
Lösung: Der Testname ist aussagekräftig, die Variablennamen sprechen, die AAA‑Struktur ist klar und die Assertion hat eine Fehlermeldung.
5. Integration: Tests in die Build‑Pipeline einbinden
Schreibe oder erweitere ein Projekt so, dass die Unit‑Tests automatisch beim Build laufen. In einem Maven‑Projekt geschieht das mit mvn test, in einem Gradle‑Projekt mit gradle test, in einem Node.js‑Projekt mit npm test.
Aufgabe: Konfiguriere eine CI‑Pipeline, die bei jedem Commit die Tests ausführt und bei einem Fehlschlag den Build stoppt.
Lösung: Ein einfacher GitHub‑Actions‑Workflow kann so aussehen:
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
- run: mvn test
6. Selbstkontrolle
Stelle Dir am Ende folgende Fragen:
- Ist jeder Test isoliert und deterministisch?
- Sind die Testnamen so beschreibend, dass Du sie als Dokumentation lesen kannst?
- Prüfen Deine Tests Verhalten statt Implementierungsdetails?
- Verwendest Du Testdoubles für externe Abhängigkeiten?
- Laufen alle Tests mit einem einzigen Befehl?
Wenn Du alle Punkte mit Ja beantworten kannst, hast Du die wichtigsten Eigenschaften guter Unit‑Tests verinnerlicht.
Themenanalyse
- Technischer Kern: AAA‑Struktur, Testdoubles, Assertions, Naming, Isolation
- Herausforderungen: Aufwand für Testdoubles, Vermeidung von Over‑Engineering, flache Tests
- Qualitätssicherung: Frühe Fehlererkennung, Refactoring‑Sicherheit, lebende Dokumentation
- Wirtschaftlichkeit: Schnelles Feedback, geringere Ausfallzeit, bessere Wartbarkeit
- Prüfungsrelevanz: IHK und Berufspraxis fragen gezielt nach Eigenschaften guter Unit‑Tests und Testdoubles
Wichtigste Quellen
- https://martinfowler.com/articles/practical-test-pyramid.html
- https://junit.org/junit5/docs/current/user-guide/
- https://testing.googleblog.com



