Heise 17.07.2026
09:00 Uhr

Won’t fix! – Teil 5: Tests schaffen Vertrauen, beweisen aber nie Korrektheit


100 Prozent Code Coverage und alle Tests grün geben kein verlässliches Korrektheitsurteil. Hinter dieser Beobachtung steckt mehr als nur Pech.

Won’t fix! – Teil 5: Tests schaffen Vertrauen, beweisen aber nie Korrektheit

Geht es um Codequalität, ist die Code Coverage eine besonders verführerische Metrik. Sie ist leicht zu erheben, in Pipelines durchzusetzen und scheint auf den ersten Blick eine objektive Aussage darüber zu treffen, wie gründlich ein System getestet ist. Bei näherer Betrachtung zeigt sich aber, dass das Testen selbst an fundamentale Grenzen stößt, an denen auch die ausgefeilteste Coverage-Messung nichts ändern kann. Das Halteproblem aus Teil 2 spielt dabei eine neue Rolle und wie wir in Teil 3 gesehen haben, lässt sich Codequalität ohnehin nicht in eine einzige Zahl pressen.

Vor einigen Jahren entwickelte ein Entwicklungsteam meines Unternehmens eine Implementierung des Chord-Algorithmus, eines klassischen Verfahrens für strukturierte Peer-to-Peer-Netzwerke. Chord organisiert die teilnehmenden Knoten in einem virtuellen Ring und erlaubt es, Key-Value-Paare effizient zu speichern und wiederzufinden, auch wenn Knoten kommen und gehen. Das Team arbeitete sorgfältig: Alle Unit-Tests waren grün, die Code Coverage lag bei 100 Prozent.

Trotzdem hatte die Implementierung einen Bug. Wurde ein Knoten im laufenden Netzwerk abgeschossen, stürzten die übrigen Knoten dominoartig ab. Das Problem lag nicht im Code eines einzelnen Knotens, sondern im Zusammenspiel mehrerer Knoten. Bestimmte Konstellationen, in denen Nachrichten zwischen Knoten in einer ungünstigen Reihenfolge eintrafen, führten zu logischen Widersprüchen, die beim Betrachten eines einzelnen Knotens nicht erkennbar waren. Die Tests deckten jede einzelne Funktion vollständig ab, aber sie testeten nicht das, was am Ende den Ausfall verursachte.

Diese Erfahrung steht stellvertretend für ein viel allgemeineres Phänomen. Tests können viele Bugs finden, aber sie können nicht garantieren, dass keine mehr da sind. Und das ist nicht nur eine pragmatische Einschränkung, sondern hat tiefer liegende Ursachen.

Code Coverage misst, wie viel Prozent des Codes durch Tests ausgeführt werden. Eine Zeile, die mindestens einmal im Rahmen eines Tests durchlaufen wird, gilt als abgedeckt. Das klingt zunächst sinnvoll, denn nicht ausgeführter Code ist mit Sicherheit nicht getestet. Die Umkehrung gilt aber nicht: Ausgeführter Code ist nicht automatisch geprüfter Code.

Ein einfaches Beispiel zeigt das Problem. Eine Funktion gibt eine Liste sortiert zurück. Ein Test ruft diese Funktion mit einer beliebigen Liste auf, prüft aber nicht das Ergebnis. Die Coverage-Messung stellt fest, dass alle Zeilen der Funktion durchlaufen wurden, und meldet 100 Prozent Abdeckung. Trotzdem hätte die Funktion auch eine zufällige Permutation zurückgeben können, ohne dass der Test es bemerkt hätte. Coverage misst Ausführung, nicht Verifikation.

Doch selbst wenn die Tests ihre Ausgaben gewissenhaft prüfen, bleibt eine zweite Lücke. Coverage misst die Strukturen des Codes, nicht die Interaktionen zwischen Komponenten. Im Chord-Beispiel waren die einzelnen Funktionen jedes Knotens vollständig abgedeckt, aber das Zusammenspiel mehrerer Knoten in spezifischen zeitlichen Konstellationen war es nicht. Was Coverage nicht erfasst, ist die kombinatorische Explosion der möglichen Reihenfolgen, in denen Nachrichten ausgetauscht werden, in denen Threads parallel laufen, in denen Knoten verbunden oder getrennt sind. Eine Coverage von 100 Prozent ist deshalb keine Aussage über die Korrektheit des Systems, sondern eine Aussage darüber, dass kein Code übersehen wurde.

Diese strukturelle Asymmetrie zwischen Tests und Korrektheit hat Edsger Dijkstra bereits 1969 auf einer NATO-Konferenz zur Softwaretechnik in einem inzwischen berühmten Satz auf den Punkt gebracht: Testing shows the presence, not the absence of bugs. Wenig später formulierte er die ausführlichere Variante in seinen „Notes on Structured Programming“: Programmtests können benutzt werden, um die Anwesenheit von Fehlern zu zeigen, niemals aber deren Abwesenheit.

Die Aussage klingt banal, hat aber weitreichende Konsequenzen. Ein Test, der einen Bug findet, beweist die Existenz dieses Bugs. Ein Test, der keinen Bug findet, beweist nichts darüber, ob weitere Bugs existieren. Die übliche Beweisstruktur in den Naturwissenschaften, dass das Ausbleiben eines Gegenbeispiels die Hypothese stützt, gilt hier nur in sehr eingeschränktem Sinne. Solange nicht alle möglichen Eingaben und Zustände durchgetestet wurden, bleibt jeder Bereich eine potenzielle Fehlerquelle. Und alle möglichen Eingaben und Zustände durchzutesten, ist in praktisch jedem nicht-trivialen Programm aussichtslos.

Schon einfache Funktionen haben mehr mögliche Eingabekombinationen, als sich vollständig durchprobieren lassen. Eine Funktion, die drei Ganzzahlen mit jeweils 32 Bit entgegennimmt, hat 2 hoch 96 mögliche Eingabekombinationen, ungefähr 80 Quadrillionen. Selbst bei einer Milliarde Tests pro Sekunde würde es mehr als zwei Billionen Jahre dauern, alle Kombinationen zu testen, ein Vielfaches des Alters des Universums. Und das ist eine triviale Funktion. Sobald Strings, Listen oder beliebige Objekte als Eingaben hinzukommen, wird der Eingaberaum buchstäblich unendlich groß.

Dieses Phänomen wird als Zustandsraumexplosion bezeichnet. Bei zustandsbehafteten Programmen, also fast jedem realen System, kommt eine weitere Dimension hinzu: Das Verhalten hängt nicht nur von der aktuellen Eingabe ab, sondern auch vom internen Zustand, der wiederum durch frühere Eingaben geprägt ist. Eine Webanwendung mit zwanzig Sessions, jeweils mit unterschiedlich gesetzten Variablen, hat einen Zustandsraum, der sich nicht mehr sinnvoll überschauen lässt. Bei verteilten Systemen wie im Chord-Beispiel multipliziert sich der Zustandsraum eines einzelnen Knotens mit der Anzahl der Knoten und ihrer möglichen Kommunikationsmuster. Was beim Einzelknoten noch handhabbar wirkte, wird im Verbund unbeherrschbar.

Die auf Developer Experience (DX) und Platform Engineering spezialisierte CLC-Konferenz findet vom 11. bis 12. November 2026 in Mannheim statt. Sie legt den Fokus darauf, wie Agentic AI die Arbeit von Developern, Software-Architekten, DevOps- und Platform Engineers verändert und sich digitale Souveränität nachhaltig erreichen lässt.

Jetzt Tickets zum Frühbucherpreis sichern.

Die übliche Antwort auf dieses Problem heißt Äquivalenzklassen. Statt jede Eingabe einzeln zu testen, werden Eingaben in Klassen zusammengefasst, die aus Sicht des Programms gleich behandelt werden sollten. Statt alle ganzen Zahlen zu testen, reichen ein typischer positiver Wert, ein typischer negativer Wert, die Null und die Grenzwerte. Das ist pragmatisch und meistens ausreichend, aber es setzt voraus, dass die Klassenbildung korrekt ist, dass also alle relevanten Fälle erkannt wurden. Genau hier liegen viele Bugs verborgen: in den Fällen, deren Eigenständigkeit niemand erkannt hat.

Selbst wenn sich der Eingaberaum vollständig durchprobieren ließe, bliebe eine zweite, oft übersehene Frage. Woher weiß ein Test, was die richtige Ausgabe ist? Diese Frage ist als Orakel-Problem in der Testtheorie bekannt. Ein Test besteht im Kern aus zwei Teilen: einer Eingabe und einer Erwartung an die Ausgabe. Die Eingabe lässt sich automatisch generieren, die Erwartung im Allgemeinen nicht.

Bei mathematischen Funktionen mag das Orakel offensichtlich sein. Eine Sortierfunktion soll eine sortierte Liste liefern, das lässt sich prüfen. Eine Quadratwurzel soll eine Zahl liefern, die mit sich selbst multipliziert die Eingabe ergibt. Bei realen Anwendungen ist die Lage komplizierter. Was ist die richtige Antwort einer Suchmaschine auf eine bestimmte Anfrage? Was ist die richtige Reihenfolge in einem Newsfeed? Was ist die richtige Empfehlung in einem Webshop? In all diesen Fällen gibt es kein objektives Orakel, sondern nur Heuristiken, Spezifikationen, Geschäftsregeln, manchmal nur menschliches Urteil. Tests können diese Orakel approximieren, aber sie sind selbst eine Quelle möglicher Fehler.

Im Chord-Beispiel zeigt sich das Orakel-Problem in einer besonders perfiden Variante. Die einzelnen Funktionen jedes Knotens hatten klar definierte Erwartungen, und die Tests prüften diese Erwartungen korrekt. Aber das richtige Verhalten des Systems beim Ausfall eines Knotens war nirgendwo als Orakel formuliert, weil niemand explizit darüber nachgedacht hatte. Tests können nur prüfen, was jemand vorher als Erwartung formuliert hat. Was niemand erwartet, kann auch niemand testen.

Aus Teil 2 dieser Serie ist das Halteproblem bereits bekannt: Alan Turing hat 1936 bewiesen, dass es kein allgemeines Verfahren geben kann, das für ein beliebiges Programm und eine beliebige Eingabe entscheidet, ob das Programm terminiert oder in eine Endlosschleife gerät. Im Kontext des Testens taucht dieses Resultat in einer subtileren Variante auf.

Wenn ein Test einen Code aufruft, dann muss die Test-Infrastruktur entscheiden, wann sie diesen Aufruf abbricht. Bleibt das Programm einfach hängen, läuft auch der Test ewig. Das übliche Mittel ist ein Timeout: Nach einer bestimmten Zeit gilt der Test als fehlgeschlagen. Dieses Vorgehen ist eine Heuristik, kein Beweis. Ein Test mit Timeout kann nicht zwischen einem Programm, das in einer Endlosschleife steckt, und einem Programm, das einfach nur sehr lange braucht, unterscheiden. Beide werden gleichermaßen als Fehler gemeldet, oder beide als Erfolg, je nach Konfiguration. Die fundamentale Unentscheidbarkeit des Halteproblems schlägt hier auf eine ganz praktische Frage durch: Wie lange darf ein Test laufen, bevor er als gescheitert gilt?

Eine verwandte Konsequenz betrifft die Generierung von Tests. Werkzeuge, die Tests automatisch erzeugen wollen, müssen entscheiden, welche Pfade durch den Code überhaupt erreichbar sind. Diese Entscheidung ist im Allgemeinen ebenfalls unentscheidbar, was aus Rice’s Theorem folgt, das bereits in Teil 2 eine Rolle spielte. Praktische Werkzeuge nähern sich dem mit Heuristiken an, aber sie können nicht garantieren, alle relevanten Pfade zu finden.

Trotz dieser fundamentalen Grenzen gibt es Verfahren, die deutlich mehr leisten als klassisches Beispiel-basiertes Unit-Testing. Sie verfolgen unterschiedliche Strategien, um den Eingaberaum strukturiert zu erschließen, auch wenn er sich nicht vollständig durchprobieren lässt.

Beim Property-based Testing werden nicht einzelne Beispiele getestet, sondern Eigenschaften, die für alle Eingaben gelten sollen. Statt zu prüfen, dass das Sortieren der Liste [3, 1, 2] das Ergebnis [1, 2, 3] liefert, wird die Eigenschaft formuliert, dass das Ergebnis einer Sortierung dieselben Elemente enthält wie die Eingabe und in aufsteigender Reihenfolge vorliegt. Das System generiert dann automatisch eine große Zahl zufälliger Eingaben und prüft die Eigenschaft. Wird ein Gegenbeispiel gefunden, versucht das Werkzeug, es zu einem minimalen Fall zu reduzieren, der den Bug noch zeigt. Property-based Testing wurde mit dem Werkzeug QuickCheck für Haskell populär und ist inzwischen für viele Programmiersprachen verfügbar.

Beim Fuzzing wird das System mit zufällig generierten oder mutierten Eingaben bombardiert, oft mit dem Ziel, Abstürze, Sicherheitslücken oder unerwartete Zustände zu finden. Moderne Fuzzer nutzen Coverage-Informationen, um Eingaben zu bevorzugen, die neue Codepfade auslösen, und arbeiten sich auf dem Weg systematisch in den Eingaberaum vor. Fuzzing hat zahlreiche schwerwiegende Bugs in weitverbreiteten Bibliotheken aufgedeckt, etwa in Browsern, Bildverarbeitungs-Bibliotheken und Kryptographie-Implementierungen.

Beim Mutation Testing schließlich werden nicht die Programme, sondern die Tests selbst getestet. Das Werkzeug nimmt kleine Änderungen am Quellcode vor, etwa wird ein Plus zu einem Minus, ein Größer-als zu einem Kleiner-als, eine Bedingung negiert. Anschließend werden die Tests gegen den derart mutierten Code laufen gelassen. Wenn die Tests die Mutation nicht bemerken, ist das ein Hinweis darauf, dass die Tests die betroffene Stelle nicht wirklich prüfen, sondern den Code nur durchlaufen lassen. Mutation Testing schließt damit genau die Lücke, die Coverage offenlässt: Es misst nicht, ob Code ausgeführt wurde, sondern ob seine Korrektheit tatsächlich überprüft wird.

All diese Verfahren erweitern die Möglichkeiten des Testens erheblich. Aber keines von ihnen löst das fundamentale Problem. Property-based Testing kann unendlich viele Eingaben nicht in endlicher Zeit prüfen, sondern nur stichprobenartig. Fuzzing findet viele Bugs, aber nicht alle. Mutation Testing prüft die Tests, aber das Orakel-Problem bleibt. Die Verfahren machen Tests besser, aber sie heben die Grenzen nicht auf.

Der Weg vom Chord-Beispiel über Code Coverage, Dijkstras Bonmot, Zustandsraumexplosion, Orakel-Problem und Halteproblem zeigt eine durchgängige Linie. Tests können niemals beweisen, dass ein Programm korrekt ist. Sie können nur Vertrauen aufbauen. Dieses Vertrauen ist nicht wertlos, im Gegenteil: Es ist der pragmatische Ausweg aus einer Reihe theoretischer Unmöglichkeiten. Aber es darf nicht mit einem Beweis verwechselt werden.

Was diese Einsicht in der Praxis bedeutet, lässt sich in wenige Punkte fassen. Tests sollten so geschrieben werden, dass sie aussagekräftige Eigenschaften prüfen, nicht nur Code zur Ausführung bringen. Eine hohe Coverage ist ein notwendiges, aber kein hinreichendes Kriterium für gute Tests. Verschiedene Testarten ergänzen sich, denn Unit-Tests, Integrationstests, End-to-End-Tests, Property-based Tests und Fuzzing decken jeweils unterschiedliche Klassen von Fehlern ab. Und vor allem: Wenn ein System produktiv geht, sollte das Vertrauen in die Tests durch Mechanismen ergänzt werden, die mit den verbleibenden Fehlern umgehen können. Monitoring, Observability, Circuit Breaker, Rollback-Strategien und Chaos Engineering sind keine Eingeständnisse mangelnder Testqualität, sondern Ausdruck der Einsicht, dass Tests allein nicht reichen können.

Im Chord-Beispiel war die Lehre genau diese: Die Tests selbst waren nicht falsch, sie hatten nur einen anderen Geltungsbereich als das Team angenommen hatte. Erst der Betrieb mit mehreren Knoten und gezielt herbeigeführten Ausfällen brachte das Problem zutage. Das ist kein Versagen des Testens, sondern eine Konsequenz seiner prinzipiellen Grenzen.

(mro)