Zum Inhalt springen
Topos Uranos

Zusammenfassung

Ein Programm, das kompiliert und etwas Vernünftiges ausgibt, ist deshalb noch nicht getestet. Diese Lektion lehrt, methodisch zu testen: die Fälle aus der Spezifikation abzuleiten, nach Bereichen und Grenzen; sie in einem Programm automatisierter Tests zusammenzufassen, das die Fehlschläge zählt und meldet; mit assert zu prüfen, was nicht eintreten kann, wenn das Programm korrekt ist, und mit static_assert zur Kompilierzeit, was eine constexpr-Funktion berechnen muss; und die Tests mit den Sanitizern auszuführen, die das undefinierte Verhalten aufdecken, das ein korrektes Ergebnis verbirgt. Alles beruht auf einer Warnung: Ein Test kann die Anwesenheit eines Fehlers zeigen, niemals seine Abwesenheit.

Lernziele

  1. Die Testfälle aus der Spezifikation einer Funktion ableiten: die Partitionen ihrer Eingabe, die Grenzen zwischen ihnen und die Sonderfälle, jeweils mit dem Ergebnis, das die Nachbedingung verlangt.
  2. Ein Programm automatisierter Tests schreiben, das die erhaltenen Ergebnisse mit den erwarteten vergleicht, jeden Fehlschlag meldet, ohne anzuhalten, und mit einer Zusammenfassung und einem Beendigungscode endet, der sie widerspiegelt.
  3. assert verwenden, um Vorbedingungen und Invarianten zu prüfen, und erklären, warum es beim Kompilieren mit -DNDEBUG verschwindet und warum seine Bedingung keine Nebeneffekte haben darf.
  4. static_assert mit constexpr-Funktionen verwenden, um Ergebnisse zur Kompilierzeit zu testen, und die Tests mit den Sanitizern ausführen, um das undefinierte Verhalten aufzudecken, das die Ergebnisse nicht offenbaren.

Was Testen ist

Testen heißt, ein Programm mit ausgewählten Eingaben auszuführen und das, was es tut, mit dem zu vergleichen, was es hätte tun sollen. Jeder Testfall besteht aus zwei Teilen: einer Eingabe, die die Vorbedingung erfüllt, und dem Ergebnis, das die Nachbedingung für sie verlangt, im Voraus und auf einem anderen Weg berechnet (von Hand, mit einer anderen Formel, mit einer bekannten Tabelle). Ohne erwartetes Ergebnis gibt es keinen Test, sondern nur eine Ausführung: die Ausgabe anzusehen und sie „vernünftig“ zu finden, ist ein Eindruck, kein Vergleich.

Nun lässt eine Funktion mit einem int-Argument mehr als vier Milliarden Eingaben zu, und eine mit zwei Argumenten etwa 1,8⋅10191{,}8 \cdot 10^{19} Paare; keine Testreihe durchläuft sie alle. Wenn dem so ist, untersuchen die Tests einen winzigen Teil der möglichen Fälle, und daraus, dass alle bestehen, folgt nicht, dass die Funktion korrekt ist. Edsger Dijkstra formulierte es 1970: Tests können die Anwesenheit von Fehlern zeigen, niemals ihre Abwesenheit. Die Unterscheidung ist die, die der Kurs Mathematische Logik I zwischen dem Überprüfen von Fällen und dem Beweisen trifft: zu prüfen, dass eine Formel für einige Werte wahr ist, beweist nicht, dass sie es für alle ist, während ein Beweis (in diesem Kurs die Invariante der Lektion Korrekte Schleifen entwerfen) alle Fälle auf einmal abdeckt. Tests ersetzen dieses Denken nicht: Sie finden die Fehler, die es übersehen hat.

Daraus folgt das Kriterium für die Wahl der Fälle. Wenn Tests die Korrektheit nicht bestätigen können, liegt ihr Wert darin, Fehler zu finden, und ein guter Testfall ist derjenige, der am wahrscheinlichsten fehlschlägt. Ein Fall, den man gewählt hat, weil man wusste, dass er funktionieren würde, sagt nichts aus. Das folgende Programm zeigt es an einer Funktion, die das größte Element eines nichtleeren Vektors zurückgeben muss.

C++-Programm
#include <print>#include <vector>​// Größtes Element einer Liste von Daten.// Vorbedingung: values ist nicht leer.// Nachbedingung: gibt das größte Element von values zurück.int largest(const std::vector<int>& values){    int best{0};    for (const int v : values) {        if (v > best) {            best = v;        }    }    return best;}​int main(){    std::println("{}", largest({3, 9, 4}));    std::println("{}", largest({7}));    std::println("{}", largest({1, 2, 3, 4}));    std::println("{}", largest({-5, -2, -8}));}
Ausgabe
9740

Die ersten drei Fälle sind die, die jeder zuerst schreibt, und die Funktion besteht sie: 9, 7 und 4 sind korrekt. Der vierte ergibt 0, einen Wert, der nicht einmal im Vektor vorkommt, obwohl er −2-2 ergeben müsste. Der Fehler steht in Zeile 9: best beginnt bei 0 und nicht beim ersten Element, sodass die Funktion nur korrekt ist, wenn irgendein Element positiv oder null ist. Die typischen Fälle teilten diese Eigenschaft, ohne dass jemand sie gewählt hätte; der vierte verletzt sie absichtlich. Die Korrektur ist int best{values[0]};, die die Vorbedingung legitim macht, weil sie garantiert, dass values[0] existiert.