6. Teststrategien I
Teststrategien I
Abschnitt betitelt „Teststrategien I“Einzelne Tests zu schreiben ist ein Handwerk. Eine Teststrategie ist eine Entscheidung: Was prüfen wir, auf welcher Ebene, mit welchem Werkzeug - und was bewusst nicht? Denn testen ist nicht umsonst. Jeder Test kostet beim Schreiben Zeit und bei jeder Änderung erneut. Das Ziel ist nicht möglichst viel Test, sondern möglichst viel Sicherheit pro investierter Minute. Dieses Kapitel legt die Grundlagen dafür; das nächste macht daraus eine dokumentierte Strategie für das Jahresprojekt.
Die Testebenen
Abschnitt betitelt „Die Testebenen“Tests unterscheiden sich danach, wie viel vom System sie auf einmal prüfen.
| Ebene | Prüft | Beispiel | Geschwindigkeit |
|---|---|---|---|
| Unit | eine Einheit isoliert (eine Funktion, eine Klasse) | die Mengenprüfung im Service | sehr schnell (Millisekunden) |
| Integration | mehrere Teile im Zusammenspiel | Service zusammen mit echter Datenbank | mittel |
| End-to-End (E2E) | das ganze System wie ein Nutzer | Klick durch die Oberfläche bis zur Datenbank und zurück | langsam (Sekunden) |
Jede Ebene beantwortet eine andere Frage. Der Unit-Test sagt “diese Regel stimmt”. Der Integrationstest sagt “diese Teile passen zusammen”. Der E2E-Test sagt “der Nutzer kann diese Aufgabe erledigen”. Keine Ebene ersetzt eine andere: Ein grüner Unit-Test garantiert nicht, dass die Teile zusammenpassen, ein grüner E2E-Test sagt nicht, welche Einheit kaputt ist.
Die Testpyramide
Abschnitt betitelt „Die Testpyramide“Die Ebenen unterscheiden sich in Kosten und Aussagekraft, und daraus folgt eine Empfehlung für ihr Mengenverhältnis:
flowchart TD E["E2E: wenige<br/>langsam, breit, aber teuer und wackelig"] I["Integration: einige<br/>mittlere Kosten, prueft Zusammenspiel"] U["Unit: viele<br/>schnell, guenstig, praezise lokalisierbar"] E --- I --- U
Die Form ist eine Pyramide, keine Säule und kein auf dem Kopf stehendes Dreieck: viele schnelle Unit-Tests unten, einige Integrationstests in der Mitte, wenige E2E-Tests oben. Der Grund ist wirtschaftlich:
- Unit-Tests sind billig, schnell und zeigen bei einem Fehler sofort die Stelle. Von ihnen kann man Hunderte haben.
- E2E-Tests sind teuer, langsam und sagen bei einem Fehlschlag nur “irgendwo im ganzen Ablauf stimmt etwas nicht”. Sie sind wertvoll, aber jeder einzelne kostet.
Die typische Fehlform ist die Eistüte (viele E2E-Tests, kaum Unit-Tests): Die Suite läuft minutenlang, ist bei jedem Fehlschlag mühsam zu deuten und wird deshalb bald ignoriert. Wer testet, wo der Fehler entsteht - meist in der Fachlogik -, braucht oben nur noch wenige Tests, die zeigen, dass alles zusammen läuft.
Werkzeuge im Framework-Umfeld
Abschnitt betitelt „Werkzeuge im Framework-Umfeld“Die Wahl der Werkzeuge wird begründet, nicht dem Zufall überlassen. Grobe Zuordnung für ein TypeScript/Next.js-Projekt:
| Zweck | Art von Werkzeug | Was es prüft |
|---|---|---|
| Unit- und Integrationstests | Testrunner (z. B. Vitest) | Funktionen, Klassen, Services; auch gegen eine echte Test-Datenbank |
| API-Tests | Testrunner plus HTTP-Aufrufe | Endpunkte mit Statuscodes und Antwortformaten |
| Komponententests | Testrunner plus Rendering-Bibliothek | eine UI-Komponente isoliert |
| End-to-End | Browser-Automatisierung (z. B. Playwright) | ganze Nutzerpfade im echten Browser |
Das Auswahlkriterium ist immer dasselbe: Passt das Werkzeug zur Ebene, und ist es in der Pipeline (Kapitel 7) automatisch ausführbar? Ein Werkzeug, das nur auf einem Rechner von Hand läuft, sichert nichts ab.
Isolieren mit Test-Doubles
Abschnitt betitelt „Isolieren mit Test-Doubles“Ein Unit-Test soll eine Einheit prüfen. Sobald diese Einheit von einer anderen abhängt - der Service vom Repository, das Repository von der Datenbank -, muss die Abhängigkeit im Test ersetzt werden, sonst prüft man beide zugleich und der Test wird langsam und unzuverlässig.
Der Sammelbegriff für solche Ersatzobjekte ist Test-Double. Die wichtigsten Arten:
| Art | Verhalten |
|---|---|
| Stub | liefert feste, vorbereitete Antworten |
| Mock | wie ein Stub, merkt sich aber zusätzlich, wie er aufgerufen wurde (für Prüfungen) |
| Fake | eine echte, aber vereinfachte Implementierung (etwa ein In-Memory-Repository) |
Der Grund, warum das im Unterricht so leicht fällt, ist die Architektur mit Interfaces: Weil der Service ein Repository-Interface verlangt und nicht eine konkrete Datenbank, lässt sich im Test jede Ersatzimplementierung übergeben.
Ein handgeschriebener Stub
Abschnitt betitelt „Ein handgeschriebener Stub“interface ClipRepository { findById(id: number): Clip | undefined; listAll(): Clip[];}
class LibraryService { constructor(private repo: ClipRepository) {}
totalDuration(): number { return this.repo.listAll().reduce((sum, clip) => sum + clip.durationSeconds, 0); }}
// the stub returns fixed data, nothing moreclass StubRepository implements ClipRepository { constructor(private clips: Clip[]) {} findById(id: number) { return this.clips.find((clip) => clip.id === id); } listAll() { return this.clips; }}import { describe, expect, it } from "vitest";
describe("LibraryService", () => { it("sums the durations", () => { const service = new LibraryService( new StubRepository([aClip({ durationSeconds: 30 }), aClip({ id: 2, durationSeconds: 90 })]), ); expect(service.totalDuration()).toBe(120); // no database involved });});Ein Mock mit dem Testrunner
Abschnitt betitelt „Ein Mock mit dem Testrunner“Ein Mock nimmt einem das Schreiben der Ersatzklasse ab und erlaubt zusätzlich, den Aufruf zu prüfen:
import { expect, it, vi } from "vitest";
it("asks the repository exactly once", () => { const repo: ClipRepository = { findById: vi.fn().mockReturnValue(aClip({ id: 7, title: "Intro" })), listAll: vi.fn().mockReturnValue([]), }; const service = new LibraryService(repo);
expect(service.titleOf(7)).toBe("Intro"); expect(repo.findById).toHaveBeenCalledWith(7); // verify the interaction expect(repo.findById).toHaveBeenCalledTimes(1);});Testdaten
Abschnitt betitelt „Testdaten“Tests brauchen Eingabedaten, und die Art, sie zu erzeugen, entscheidet über die Lesbarkeit der ganzen Suite.
Das Problem bei kopierten Testobjekten: Jeder Test schleppt alle Felder mit, auch die, die für ihn irrelevant sind, und beim kleinsten Modellwechsel müssen Dutzende Stellen angefasst werden. Die Lösung ist ein Builder (oder eine Fabrikfunktion) mit sinnvollen Vorgaben, der nur das Relevante überschreiben lässt:
function aClip(overrides: Partial<Clip> = {}): Clip { return { id: 1, title: "untitled", durationSeconds: 60, ...overrides };}
aClip(); // a valid default clipaClip({ durationSeconds: 30 }); // same, but with the one field that matters hereSo steht in jedem Test nur, was für ihn wesentlich ist - aClip({ durationSeconds: 30 }) sagt “hier geht es um die Dauer” und verschweigt den Rest. Das ist zugleich Dokumentation: Der Test zeigt, welche Eigenschaft er tatsächlich prüft.
Lernergebnisse: Was Sie nach diesem Kapitel können sollten
Abschnitt betitelt „Lernergebnisse: Was Sie nach diesem Kapitel können sollten“Nach Abschluss dieses Kapitels sollten Schülerinnen und Schüler in der Lage sein:
- Erklären: die Testebenen Unit, Integration und End-to-End unterscheiden und ihre jeweilige Aussage benennen.
- Erklären: die Testpyramide mit Kosten, Geschwindigkeit und Aussagekraft begründen und die Fehlform “Eistüte” erkennen.
- Beurteilen: für einen Anwendungsteil die passende Testebene und ein begründetes Werkzeug wählen.
- Anwenden: Abhängigkeiten mit Stubs, Mocks oder Fakes isolieren und die Arten unterscheiden.
- Beurteilen: entscheiden, was mit echten Objekten und was an den Rändern ersetzt getestet wird.
- Anwenden: Testdaten mit einem Builder erzeugen und Flakiness durch Ausschalten von Zufall und Zeit vermeiden.
Passende Übungen
Abschnitt betitelt „Passende Übungen“- Aufgabe 09 - Die Pyramide fürs Projekt