Jenseits des Abdeckungsprozentsatzes: Prüfung der Fehler, auf die Nutzer tatsächlich stoßen
Warum eine hohe Code-Coverage-Zahl ungetestete Fehlermöglichkeiten verbergen kann, die drei Arten von sinnlosen Tests, die sie fördert, und wie man Tests schreibt, die das tatsächliche Verhalten schützen.
Ein Pull Request mit grünen Tests und einem Abdeckungsbericht von über 98 % wirkt sicher. Doch dann gibt eine API null zurück, obwohl die Benutzeroberfläche ein Objekt erwartet hat, eine Anfrage wird bei einer langsamen Mobilverbindung aus Versehen zu früh abgeschlossen, oder jemand klickt zweimal auf die Absenden-Taste – daraufhin bricht die Frontend-Anwendung in einem leeren Bildschirm zusammen. Die Frage nach den Ursachen ist stets dieselbe: Wie konnte das fehlschlagen, obwohl der Code abgedeckt war? Dieser Artikel erklärt, was Abdeckungsgrad tatsächlich misst, welche Testmuster ihn erhöhen, ohne Schutz zu bieten, und wie man den Testumfang auf die wirklich wichtigen Fehler ausrichten kann.
Was der Abdeckungsgrad misst und was nicht
Ein Abdeckungstool protokolliert, welche Zeilen, Branches und Funktionen während des Laufs der Tests ausgeführt wurden. Das ist alles. Es weiß nicht, ob irgendwelche Prüfungen das Ergebnis überprüft haben, ob die Eingaben realen Datenverkehr nachempfunden waren oder ob der Code bei Fehlverhalten einer Abhängigkeit korrekt funktioniert. Ausgeführt werden und überprüft werden sind zwei verschiedene Eigenschaften, und die Abdeckung gibt nur über das Erste Auskunft.
Eine nützliche Analogie ist eine Gebäudeinspektion, bei der jemand mit einer Taschenlampe jeden Raum durchgeht. Jeder Raum wurde besucht, doch niemand hat das Dach bei Sturm getestet. Hohe Abdeckung zeigt lediglich an, dass Ihre Tests den Code besucht haben, nicht jedoch, dass sie ihn auf seine Zuverlässigkeit getestet haben.
Drei Muster, die die Abdeckung erhöhen, ohne Sicherheit hinzuzufügen
Wenn ein Prozentsatz zum Ziel wird, optimieren die Menschen für diesen Prozentsatz. Das Ergebnis sind Tests, die das Tool mit minimalem Aufwand zufriedenstellen. Drei Muster tauchen immer wieder auf.
Die Illusion des einfachen Szenarios
Stellen Sie sich einen Helper vor, der Eingaben des Benutzers analysiert und den Zustand aktualisiert. Sein Test verwendet eine saubere, korrekt formatierte Zeichenkette, überprüft die erwartete Ausgabe und erreicht eine vollständige Zweigabdeckung. Was er jedoch niemals ausprobiert, sind leere Zeichenketten, ungewöhnliche Zeichen, ein undefined-Argument, eine langsame Antwort oder fehlerhaftes JSON. Jede Zeile wurde ausgeführt, doch keiner der Eingaben, die zu Störungen in der Produktion führen können, wurde getestet.
Die übermäßig getestete Komponente
Hier wird jeder API-Aufruf, jeder Kontextanbieter sowie jedes verschachtelte Kindelement durch einen Stub ersetzt. Der Testablauf ist in wenigen Millisekunden abgeschlossen und umfasst alle Render-Branchen. In der Produktion gibt der echte Endpunkt jedoch eine leicht andere Struktur zurück, als die Simulation angenommen hat, oder eine Bibliothek ändert nach einem Upgrade ihre Art und Weise, Ereignisse auszusenden. Die Tests bestehen weiterhin, weil sie nur mit den eigenen Annahmen arbeiten, und die erste echte Integration findet im Browser des Benutzers statt.
Tests ohne aussagekräftige Prüfungen
Die schwächste Form tritt bei strengen Vorgaben auf, wie beispielsweise einem erforderlichen Durchschnitt von 90 % im gesamten Team. Die Tests rufen Funktionen lediglich auf, um Zeilenaufrufe zu registrieren, und führen kaum oder gar keine Prüfungen durch. Der Bericht zeigt grün an, obwohl es keinerlei Schutz vor zukünftigen Änderungen in der Logik gibt.
Wie man Verhalten statt Zeilen testet
Tests, die Fehler bereits vor den Nutzern aufspüren, konzentrieren sich darauf, was das System tut, und nicht darauf, welche Codezeilen es berührt.
- Es werden Testzustände und -übergänge getestet, nicht einzelne Funktionen. Den Nutzern ist es egal, ob ein Hilfsprogramm ausgeführt wurde. Sie kümmern sich darum, was passiert, wenn die Verbindung während des Absendens eines Formulars abbricht oder sie versuchen, die Seite zu verlassen, während ein Dateiupload noch läuft. Testen Sie Ladeindikatoren, Fehlerzustände, Wiederholungsversuche sowie Fehlergrenzen.
- Wählen Sie Integrationen, wo sie praktikabel sind. Das Emulieren echter externer Schnittstellen, wie z. B. eines Zahlungsgateways von Drittanbietern, ist sinnvoll. Das Emulieren eigener interner Hilfsprogramme oder der eigenen Datenschicht verdeckt meist nur die Fehler, die zwischen ihnen liegen. Lassen Sie Komponenten und Module so weit wie möglich gemeinsam laufen, solange die Kosten es zulassen.
Eine damit verbundene, etablierte Technik ist das Mutationstesten, bei dem der Code absichtlich verändert wird (z. B. eine Bedingung umgekehrt oder eine Zeile entfernt) und überprüft wird, ob dadurch ein Test fehlschlägt. Überlebende Mutationen weisen direkt auf Code hin, der ausgeführt wird, aber nicht überprüft wird – genau das kann die Lückenabdeckung nicht zeigen. Für spezifische Informationen auf Komponentenebene siehe diese React-Testing-Antipatterns, die falsches Vertrauen erwecken.
Nutzung der Abdeckungsrate als Signal, nicht als Ziel
Die Abdeckungsrate ist nicht nutzlos. Eine niedrige Zahl in einem kritischen Modul ist ein echtes Warnsignal, und ein Bericht kann Codepfade aufzeigen, die überhaupt nicht getestet werden. Das Problem beginnt, wenn die Prozentszahl zum Ziel wird – denn dadurch wird Menge vor Präzision bevorzugt.
Ein schlankes Testset mit einer Abdeckungsrate von etwa 65 Prozent, das sich auf risikoreiche Arbeitsabläufe, komplexe Geschäftsregeln sowie Fehlerbehebung konzentriert, verhindert weitaus mehr Vorfälle als ein anfälliges Testset mit 95 Prozent, das ausschließlich aus „glücklichen“ Abläufen und Mocks besteht. Bevor man einen Test hinzufügt, ist die bessere Frage statt „Welche Zeilen sind ungetestet?“: „Welchen realistischen Fehler würde dieser Test aufdecken?“
Kernpunkte
- Die Abdeckungsrate zeigt an, welcher Code ausgeführt wurde – nicht, ob sein Verhalten überprüft wurde.
- Eingaben aus „glücklichen“ Abläufen, umfangreiche interne Mocks sowie tests ohne Assertionen erhöhen alle die Zahl, ohne das Risiko zu verringern.
Verwandte Artikel
- Jenseits der Bundle-Größe: Was tatsächlich Ihre Web-Anwendung verlangsamt — Warum das Kürzen von Kilobyten selten zu einer schnelleren Anwendung führt und wie man die tatsächliche Wartezeit über Server, Workflows, Hydrationsprozesse sowie Drittanbieter-Skripte und Bilder nachvollzieht.
- Jenseits von P95: Die Messung der Latenz, die Ihre Nutzer tatsächlich erleben — Warum ein gesunder P95-Wert mit einem langsamen Produkt koexistieren kann, wie Wartezeiten und Ausbreitungszeiten vor den Dashboards verborgen bleiben und wie die Zeitmessung für jeden Schritt Spielchen bezüglich der Latenz beendet.