Einleitung
Als ich anfing, Tests zu schreiben, konzentrierte ich mich hauptsächlich auf reine Unit-Tests und folgte der sogenannten Testpyramide. Das bedeutete viele Unit-Tests und Mocking, eine Handvoll Integrationstests und nur wenige End-to-End (E2E)-Tests. Es ist ein weithin akzeptierter Ansatz, und das aus gutem Grund. Aber wie bei allem in der Software gibt es Kompromisse.
Wenn Sie mit der Testpyramide nicht vertraut sind, empfehle ich Ihnen die Lektüre von Martin Fowlers Practical Test Pyramid für einen gründlichen Überblick.
Im Laufe der Zeit habe ich die Nuancen zwischen Unit-Tests, Integrationstests und E2E-Tests schätzen gelernt. Jeder Stil dient einem anderen Zweck, und das Verständnis ihrer Stärken und Schwächen kann Ihnen helfen, bessere Entscheidungen darüber zu treffen, wo Sie Ihren Testaufwand investieren. Denn Testen ist a) teuer und b) Sie können nicht alles testen. Und nein – 100% Testabdeckung bedeutet NICHT, dass Sie alles getestet haben.
Unit-Tests: Horizontales Testen
Unit-Testing – manchmal auch “horizontales Testen” genannt – konzentriert sich auf die Überprüfung einer einzelnen Schicht in Isolation. Die Idee ist, einen Service (oder eine Funktion, oder eine Klasse) zu testen, während der Rest gemockt wird. Diese Isolation bedeutet, dass Sie wirklich nur Ihren Code testen, nicht seine Abhängigkeiten.
Vorteile:
- Fehler genau lokalisieren: Wenn ein Unit-Test fehlschlägt, wissen Sie genau, wo das Problem liegt. Cool!
- Geringerer Wartungsaufwand (normalerweise): Da jeder Test nur auf eine einzelne Komponente abzielt, brechen Änderungen in anderen Teilen Ihrer Anwendung typischerweise nicht Ihre Tests.
Nachteile:
- Refactoring-Schmerzen: Wenn Sie die Schnittstelle oder das Verhalten des Systems ändern, müssen Sie oft viele Tests umschreiben. Das Mocken von Abhängigkeiten kann auch zum Problem werden, wenn sich Ihr Code weiterentwickelt.
- Begrenzte Abdeckung: Unit-Tests könnten Probleme übersehen, die nur auftreten, wenn Komponenten interagieren.
Integrations- und End-to-End-Tests: Vertikales Testen
Am anderen Ende des Spektrums haben wir Integrationstests und End-to-End (E2E)-Tests – was Sie sich als “vertikales Testen” vorstellen können. Anstatt Komponenten zu isolieren, testen diese Tests mehrere Schichten Ihrer Anwendung zusammen, manchmal sogar den gesamten Stack von der API/dem Controller bis zur Datenbank.
In der extremsten Form schreiben einige Teams nur E2E-Tests, um das Gesamtverhalten ihrer Anwendungen zu überprüfen (siehe Yann Simons Artikel für mehr dazu).
Vorteile:
- Einfaches Refactoring: Sie können ändern, wie Services miteinander verbunden sind oder sogar ihre Aufrufreihenfolge umstellen, ohne Ihre Tests aktualisieren zu müssen – solange das externe Verhalten gleich bleibt.
- Realistische Abdeckung: Diese Tests finden mit höherer Wahrscheinlichkeit Bugs, die nur auftreten, wenn Komponenten interagieren, wie z.B. Serialisierungsprobleme oder falsch konfigurierte Abhängigkeiten.
Nachteile:
- Schwierigeres Debugging: Wenn ein Test fehlschlägt, kann es schwierig sein, genau herauszufinden, was schiefgelaufen ist, da so viele Klassen oder Services beteiligt sind.
- Wartungsaufwand: Wenn Ihre Anwendung wächst, kann die Pflege von E2E- oder breit angelegten Integrationstests zeitaufwendig sein. Das Hinzufügen neuer Features kann dazu führen, dass bestehende Tests unerwartet fehlschlagen, und es ist nicht immer klar, warum.
Ein pragmatischer Mittelweg
Persönlich verlasse ich mich selten nur auf Full-Stack-E2E-Tests. Sie sind großartig, um subtile Integrations-Bugs zu finden, aber wenn etwas schiefgeht, kann die Suche nach der Ursache frustrierend und zeitaufwendig sein.
Stattdessen bevorzuge ich Tests, die kleinen und schnellen Integrationstests ähneln. Diese Tests decken wichtige Interaktionen ab, ohne alles zu mocken. Zum Beispiel teste ich meine REST-Controller mit echten JSON-Requests, um sicherzustellen, dass Serialisierung und Deserialisierung über die Leitung korrekt funktionieren. Das erkennt Probleme, die reine Unit-Tests übersehen würden, während die Tests relativ isoliert und schnell bleiben. Auf Datenbankebene machen Tools wie TestContainers es einfach, echte Datenbankinstanzen für Ihre Tests hochzufahren. Das ermöglicht es, Repository-Code mit der echten Datenbank zu testen, genau wie in der Produktion.
Der Schlüssel ist sicherzustellen, dass Ihre Tests schnell sind und spezifische Teile Ihres Systems abdecken, um einfaches Debugging zu ermöglichen.
Fazit
Am Ende gibt es kein perfektes Rezept beim Testen. Die richtige Mischung aus Unit-, Integrations- und End-to-End-Tests hängt von Ihrem Team, Ihrer Anwendung und Ihrer Bereitschaft ab, Geschwindigkeit, Zuverlässigkeit und Abdeckung auszubalancieren. Während reine Unit-Tests Präzision und schnelles Feedback bieten, können sie reale Integrationsprobleme übersehen. Auf der anderen Seite bieten vollständige E2E-Tests Vertrauen in das Verhalten Ihrer Anwendung, können aber langsam und schwer zu diagnostizieren sein, wenn Fehler auftreten.
Der effektivste Ansatz ist oft eine pragmatische Mischung: Nutzen Sie Unit-Tests für die Kerngeschäftslogik, verwenden Sie gezielte Integrationstests, um zu überprüfen, wie Komponenten kommunizieren, und reservieren Sie End-to-End-Tests für kritische User Journeys. Tools wie TestContainers machen es einfacher denn je, die Lücke zwischen Unit- und Integrationstests zu überbrücken.
Weiterführende Lektüre:
- Practical Test Pyramid - Martin Fowlers klassischer Leitfaden zur Strukturierung Ihrer Tests
- Enlarge Your Test Scope! - Yann Simon über die Vorteile breit angelegter Tests
- TestContainers - Mein Lieblings-Framework für realistische Integrationstests
