Zum Inhalt springen

Suchen...

Testpyramide – ein kritischer Blick

Viele Unit-Tests, hohe Testabdeckung, trotzdem Bugs. Warum die Testpyramide oft falsch angewendet wird und wann andere Ansätze sinnvoller sind.

8 Min. Lesezeit
Cover für Testpyramide – ein kritischer Blick

Die Testpyramide ist kein starres Stufenmodell, sondern eine Idee: Tests verschiedener Ebenen sollen aufeinander aufbauen und sich ergänzen, statt Anforderungen doppelt abzudecken. Ihr Kerngedanke ist, Entwicklungs-Artefakte für effizienteres Testen zu nutzen. Ob das eine Pyramidenform ergibt, hängt vom Projekt ab und muss projektspezifisch entschieden werden.

Das Wichtigste in Kürze

  • Die Testpyramide wurde nicht erfunden, um möglichst viele Unit-Tests zu fordern, sondern um Testschichten so aufzubauen, dass sie sich gegenseitig ergänzen und keine Anforderung doppelt abgedeckt wird.
  • Wer Unit-Tests auf Basis falscher Abstraktionen schreibt, muss beim Refactoring nicht nur den Produktionscode, sondern auch die gesamte Testsuite anpassen, was den vermeintlichen Effizienzgewinn zunichtemacht.
  • In Microservice-Architekturen sind viele Tests, die mit JUnit geschrieben werden, faktisch Integrationstests, weil sie Controller und Service-Schichten gemeinsam prüfen, nicht einzelne Units.
  • Hohe Code-Coverage ist kein Qualitätsbeweis: Fehler entstehen oft im Zusammenspiel der Komponenten, nicht innerhalb einer einzelnen Unit, die ein Test isoliert abdeckt.
  • Architekturentscheidungen werden zu oft im testfreien Raum getroffen; eine vollständige Architekturbegründung sollte immer auch erklären, wie das System testbar bleibt.

Die Testpyramide ist eine Testidee, kein Bauplan

Die Testpyramide löst ein konkretes Problem: Große Applikationen ließen sich nicht mehr in vernünftiger Zeit testen. Der eigentliche Kern liegt nicht in der Form und nicht in der Faustregel “viele Unit-Tests unten, wenige UI-Tests oben”. Er liegt in zwei Gedanken, die im Alltag oft untergehen.

Erster Gedanke: Testen ist nichts Separates. Es wird nicht am Schluss auf das fertige Produkt gesetzt und nicht von einem eigenen Team nachgelagert erledigt. Tests werden dann effizienter, wenn sie Artefakte mitnutzen, die Entwickler ohnehin schon entworfen haben. Genau das tun Unit-Tests: Sie arbeiten auf einem niedrigeren Level als ein Blackbox-Test und erzielen mit weniger Aufwand ein vergleichbares Ergebnis.

Zweiter Gedanke: Die Schichten bauen aufeinander auf. Es geht weniger um die Pyramidenform als darum, dass die Test-Ebenen sich gegenseitig ergänzen, statt sich zu doppeln. Auf der oberen Ebene noch einmal zu testen, was unten längst geprüft wurde, bringt nichts. Unten Dinge wegzulassen und sie dann an der Spitze nachzuholen, ist genauso unsinnig.

Wer diese Ideen ernst nimmt und auf das eigene Projekt überträgt, landet selten bei einer sauberen Pyramide. Die Schichten verschieben sich aus guten Gründen. Mal braucht es mehr Integrationstests, mal genügen Blackbox-Tests. Die Form folgt dem Projekt, nicht umgekehrt.

Warum ein einprägsames Bild das Nachdenken ersetzt

Die Testpyramide hat eine bequeme Eigenschaft: Jeder kennt das Bild, jeder findet sich darin wieder, jeder hat eine Idee, was er tun soll. Was genau dahintersteckt, bleibt bei den meisten unklar.

Daraus wird ein Reflex. Am Ende vieler Vorträge steht der Satz, über allem schwebe die Testpyramide, alles Vorherige gelte nur in ihrem Kontext. Das Bild wird zur Pflichtübung, ohne dass jemand prüft, ob es zur eigenen Architektur passt.

Ob die Pyramide noch zu aktuellen Architekturen passt, ist eine offene Frage. Microservice-Landschaften, agile Vorgehensweisen und DevOps sehen anders aus als die großen UI-Applikationen, für die das Modell ursprünglich gedacht war.

Was Unit-Test heißt und was JUnit damit anrichtet

Vieles, was als Unit-Test läuft, ist in Wahrheit ein Integrationstest. Wer Tests gegen eine REST-API schreibt oder gegen den Controller und die Service-Schichten darunter, prüft kein isoliertes Unit. Er nutzt nur ein Unit-Test-Framework, um Integrationstests zu schreiben.

Genau hier liegt die Begriffsverwirrung. Werkzeuge wie JUnit oder NUnit heißen so, testen aber längst auch andere Ebenen. Weil im Namen “Unit” steht, denken viele nicht mehr darüber nach, was sie eigentlich gerade prüfen.

Eigentlich verwenden wir nur ein Unit-Test-Framework, um diese Integrationstests zu schreiben. Der entscheidende Punkt ist genau dieser Übergang: Wann schreibe ich einen Integrationstest, wann einen Unit-Test, und wie vermeide ich, dieselbe Anforderung zweimal abzutesten? — Ronald Brill

Der praktische Maßstab ist die Doppelung. Teste dieselbe Anforderung nicht auf zwei Ebenen. Sobald in einem Projekt unklar ist, was Unit-Test und was Integrationstest bedeutet, prüfst du Dinge mehrfach, ohne es zu merken.

Je leichter die Services, desto weniger gibt es zu Unit-testen

Mit leichtgewichtigen Services schrumpft der sinnvolle Anteil an Unit-Tests. Je mächtiger die zugrundeliegenden Frameworks sind, und je stärker sie selbst getestet wurden, desto weniger bleibt, das sich überhaupt isoliert testen lässt.

Unit-Tests lassen sich immer schreiben. Die Frage ist, was sie noch zeigen. Ein typisches Muster: Die Testabdeckung ist hoch, trotzdem stecken Fehler in der Software. Sieht man genauer hin, hätte der Unit-Test den Fehler gar nicht finden können, weil er nicht in der einzelnen Unit liegt, sondern im Zusammenspiel der Teile.

In einer Microservice-Landschaft verschärft sich das. Das Zusammenspiel passiert nicht mehr innerhalb eines Moduls, sondern zwischen den Services. Genau dort entstehen Fehler, die kein Unit-Test der einzelnen Komponente aufdeckt.

Dazu kommt der tote Aufwand. Viele Null-Checks und Border-Cases auf Unit-Ebene prüfen Fälle, die in einer komplexen Software nie auftreten, weil die Absicherung schon zwei Ebenen darüber stattgefunden hat.

Unit-Tests haben einen Suchtfaktor

Unit-Tests fühlen sich nach Sicherheit an, besonders für Leute, die sich ihrer Sache noch nicht sicher sind. Es gibt Metriken, man sieht, ob man alles gemacht hat, man hat das Gefühl, Qualität gebaut zu haben. Man kann sich noch einen Test ausdenken und sich selbst zeigen, dass man auch daran gedacht hat.

Im Code-Review zahlt sich das scheinbar aus. “Ich habe viele Unit-Tests, die Abdeckung ist gut” wirkt wie ein Beleg für Qualität. Der Effekt: Man beschäftigt sich weniger mit der eigentlichen Aufgabe.

Die eigentliche Aufgabe ist eine andere Frage. Habe ich das Business verstanden? Habe ich den Prozess verstanden, den mein Service oder mein UI unterstützen soll? Diese Fragen sind schwerer zu beantworten als das Hochzählen einer Coverage-Zahl.

Reporting verstärkt den Reflex. Coverage lässt sich leicht ausweisen, und eine 80-Prozent-Marke ist für Projektleiter und Testmanager attraktiv. Eine hohe Zahl sagt aber nichts darüber aus, ob die Tests etwas Sinnvolles prüfen.

Hohe Abstraktion rächt sich beim Refactoring

Zu viele Unit-Tests können ein Refactoring ausbremsen. Kommt eine neue Anforderung und muss das System umgebaut werden, ziehen sich oft sämtliche Unit-Tests mit. Der Umbau dauert deutlich länger, weil jede einzelne Unit-Test-Schicht angefasst werden muss.

Dahinter steckt häufig eine falsche Abstraktion. Stimmt die Abstraktion der Software nicht, stimmt sie auch für die Unit-Tests nicht, weil diese auf derselben niedrigen Ebene sitzen. Der vermeintliche Vorteil, nah an der Implementierung zu testen, fällt einem genau dann auf die Füße.

Die Konsequenz ist eine Abwägung. Viel in Unit-Tests zu stecken ist nicht automatisch effizient, kostengünstig oder zielführend. Manchmal ist es das Gegenteil.

Integrationstests kehren das Kostenargument um

Das klassische Argument “mach Unit-Tests, weil sie günstiger sind” gilt nicht mehr überall. Es stammt aus einer Zeit großer UI-Applikationen, in der Blackbox-Tests über die Oberfläche ein echter Schmerz waren. Wer heute in alten oder kleinen UI-Frameworks arbeitet, kennt das noch: Ein WPF-Projekt Blackbox zu testen, ist eine Katastrophe.

Bei einem REST-Service sieht die Rechnung anders aus. Solche Schnittstellen lassen sich einfach, effizient und kostengünstig testen. Frameworks wie Spring haben Testbarkeit von vornherein mitgedacht.

Damit dreht sich das Argument. Ein Integrationstest ist vielleicht nicht ganz so billig wie ein Unit-Test, aber sein Effekt ist größer. Du testest ein breiteres Spektrum und prüfst nicht Fälle, die in der Praxis nie eintreten. Im Microservice-Umfeld halten deshalb viele Integrationstests für den sinnvolleren Hebel.

Tests bauen auf Vertrauen zwischen Teams auf

Hinter jeder Testschicht steckt eine Verlass-Annahme. Wer auf einer höheren Ebene testet, verlässt sich darauf, dass die Ebene darunter ihren Teil günstiger und zuverlässig prüft. In einer Personalunion ist das stillschweigend geregelt. In größeren Teams nicht.

Dann braucht es explizite Regeln und Kommunikation. Wer einen Unit-Test ändert oder entfernt, ändert die Grundlage, auf die sich die Integrationstests darüber stützen. Eigentlich müsste das abgestimmt werden, damit die Testsuite aus Sicht der oberen Ebene vollständig bleibt.

Zwischen Services ist es derselbe Mechanismus. Ein Service ruft einen anderen Service auf. Statt jeden Sonderfall des dahinterliegenden Service erneut über Blackbox-Tests abzudecken, kannst du eine Kette aus Verantwortlichkeiten aufbauen, die auf Regeln und Vertrauen beruht. Jeder Teil deckt einen Teil der Qualitätsanforderungen ab, die Summe ergibt die ausgelieferte Qualität.

Das ist Arbeit, weil verschiedene Teams, Technologien und Hintergründe aufeinandertreffen. Je weiter hinten ein Team in der Kette steht, desto schwerer ist zu erkennen, dass es eine Verantwortung gegenüber den vorgelagerten Teams trägt. Metriken sagen darüber nichts aus. Fehlt das Vertrauen, fällt man zurück und testet wieder alles, und landet bei der unhandlichen, viel zu langsamen Testsuite, die die Pyramide ursprünglich vermeiden wollte.

Wie du eine ineffiziente Testsuite angehst

Es gibt kein Universalrezept, aber einen praktikablen Einstieg: aus jedem Fehler lernen, statt denselben Fehler zu wiederholen. Geh bewusst ins Risiko. Vollständige Abdeckung ist ohnehin nicht erreichbar, Fehler bleiben drin.

Die wirksamste Reflexion findet nach einem konkreten Fehler statt. Warum ist das passiert? Hätte ein echter Unit-Test diesen Fehler überhaupt finden können? Diese Analyse passiert in der Praxis viel zu selten, dabei macht genau sie die Testsuite besser.

Am konkreten Beispiel werden die impliziten Annahmen sichtbar. “Für mich war klar, dass ihr das testet.” “Auf die Idee wäre ich nie gekommen.” Solche Lücken zeigen sich nicht im theoretischen Raum, sondern nur, wenn man sich einen realen Fall gemeinsam ansieht.

Dafür müssen die starren Rollenbilder weichen. Tester und Entwickler gehören an einen Tisch, idealerweise auch Architekten, Designer und Requirements. Bestimmte Tests kann ein Entwickler einfacher erledigen, andere ein Tester, je näher es Richtung Blackbox geht. Beide Seiten sollten lernen, die Testfälle der anderen zu lesen. Wer den Testfall lesen kann, versteht, was geprüft wird, und weiß, welchen Beitrag er selbst zu leisten hat.

Testbarkeit gehört in die Architekturbegründung. Zu einem Architekturentwurf sollte gehören, wie sich das Ergebnis testen lässt. In der Praxis kommt die Testmöglichkeit fast immer hinterher, wenn überhaupt. Viele technische und architektonische Entscheidungen fallen im testfreien Raum.

Und manchmal braucht es Mut zum Wegwerfen. Wenn eine Testsuite mit großem Aufwand betrieben wird und trotzdem nicht funktioniert, ist es richtig zu sagen: Wir machen das anders.

Diese Seite teilen

Ähnliche Beiträge