Wenn jeder Pfad durch ein Programm getestet wird, ist dann sichergestellt, dass alle Fehler gefunden werden?
Wenn nein, warum nicht? Wie können Sie alle möglichen Kombinationen von Programmabläufen durchgehen und das Problem, falls vorhanden, nicht finden?
Ich zögere vorzuschlagen, dass "alle Fehler" gefunden werden können, aber vielleicht liegt das daran, dass die Pfadabdeckung nicht praktikabel ist (da sie kombinatorisch ist), sodass sie nie erlebt wird?
Hinweis: Dieser Artikel enthält eine kurze Zusammenfassung der Abdeckungstypen, wenn ich darüber nachdenke.
Antworten:
Nein
Denn selbst wenn Sie alle möglichen Pfade testen , haben Sie sie noch nicht mit allen möglichen Werten oder allen möglichen Wertekombinationen getestet . Zum Beispiel (Pseudocode):
quelle
Neben der Antwort von Mason gibt es noch ein weiteres Problem: Die Abdeckung gibt nicht an , welcher Code getestet wurde, sondern welcher Code ausgeführt wurde .
Stellen Sie sich vor, Sie haben eine Testsuite mit 100% Pfadabdeckung. Entfernen Sie nun alle Zusicherungen und führen Sie die Testsuite erneut aus. Voilà, die Testsuite hat immer noch eine 100% ige Pfadabdeckung, testet aber absolut nichts.
quelle
ON ERROR GOTO
ist auch ein Weg, wie C istif(errno)
.Hier ist ein einfaches Beispiel, um die Dinge abzurunden. Betrachten Sie den folgenden Sortieralgorithmus (in Java):
Jetzt lass uns testen:
Nehmen wir nun an, dass (A) dieser spezielle Aufruf
sort
das richtige Ergebnis zurückgibt, (B) alle Codepfade von diesem Test abgedeckt wurden.Aber offensichtlich sortiert das Programm nicht wirklich.
Daraus folgt, dass die Abdeckung aller Codepfade nicht ausreicht, um sicherzustellen, dass das Programm keine Fehler aufweist.
quelle
Betrachten Sie die
abs
Funktion, die den absoluten Wert einer Zahl zurückgibt. Hier ist ein Test (Python, stellen Sie sich ein Test-Framework vor):Diese Implementierung ist korrekt, erreicht jedoch nur eine Codeabdeckung von 60%:
Diese Implementierung ist falsch, wird jedoch zu 100% mit Code abgedeckt:
quelle
def abs(x): if x == -3: return 3 else: return 0
zeilenweise unterbrochene Python): Sie könnten daselse: return 0
Teil möglicherweise umgehen und eine 100% ige Abdeckung erhalten, aber die Funktion wäre im Wesentlichen nutzlos, obwohl sie den Komponententest besteht.Als weitere Ergänzung zu Masons Antwort kann das Verhalten eines Programms von der Laufzeitumgebung abhängen.
Der folgende Code enthält ein Use-After-Free:
Dieser Code ist Undefiniertes Verhalten. Abhängig von der Konfiguration (release | debug), dem Betriebssystem und dem Compiler ergeben sich unterschiedliche Verhaltensweisen. Nicht nur die Pfadabdeckung garantiert nicht, dass Sie die UAF finden, sondern Ihre Testsuite deckt in der Regel nicht die verschiedenen möglichen Verhaltensweisen der UAF ab, die von der Konfiguration abhängen.
Zum anderen ist es unwahrscheinlich, dass die Pfadabdeckung in der Praxis mit einem Programm erreicht werden kann, auch wenn die Aufdeckung aller Fehler garantiert ist. Betrachten Sie das folgende:
Wenn Ihre Testsuite alle Pfade dafür erzeugen kann, dann gratulieren Sie einem Kryptographen.
quelle
cryptohash
, ist es ein wenig schwierig zu sagen, was "ausreichend klein" ist. Vielleicht dauert es zwei Tage, bis ein Superrechner fertig ist. Aber ja,int
könnte sich als etwas herausstellenshort
.Aus den anderen Antworten geht hervor, dass 100% Code-Abdeckung in Tests nicht 100% Code-Korrektheit bedeutet, oder sogar, dass alle Fehler, die durch Tests gefunden werden konnten, gefunden werden (egal, welche Fehler kein Test finden konnte).
Eine andere Möglichkeit, diese Frage zu beantworten, ist eine aus der Praxis:
In der realen Welt und auf Ihrem eigenen Computer gibt es viele Softwareteile, die mit einer Reihe von Tests entwickelt wurden, die eine hundertprozentige Abdeckung bieten und dennoch Fehler aufweisen, einschließlich Fehler, die durch bessere Tests identifiziert werden können.
Eine damit verbundene Frage lautet daher:
Tools zur Codeabdeckung helfen dabei, Bereiche zu identifizieren, die nicht getestet wurden. Das kann in Ordnung sein (der Code ist nachweislich auch ohne Tests korrekt), es kann unmöglich sein, ihn aufzulösen (aus irgendeinem Grund kann ein Pfad nicht erreicht werden), oder es kann der Ort eines großen stinkenden Fehlers sein, entweder jetzt oder nach zukünftigen Änderungen.
In gewisser Hinsicht ist die Rechtschreibprüfung vergleichbar: Etwas kann die Rechtschreibprüfung bestehen und so falsch geschrieben werden, dass es mit einem Wort im Wörterbuch übereinstimmt. Oder es kann "scheitern", weil die richtigen Wörter nicht im Wörterbuch enthalten sind. Oder es kann vergehen und völliger Unsinn sein. Die Rechtschreibprüfung ist ein Tool, mit dem Sie Stellen identifizieren können, an denen Sie möglicherweise das Korrekturlesen verpasst haben. Ebenso wenig kann jedoch eine vollständige und korrekte Korrektur gewährleistet werden, sodass die Codeabdeckung keine vollständige und korrekte Prüfung gewährleisten kann.
Und natürlich ist der falsche Weg, die Rechtschreibprüfung zu verwenden, für jeden Vorschlag bekannt, den es vorschlägt, so dass das Ducking noch schlimmer wird, wenn das Mutterschaf ihm einen Kredit hinterlässt.
Bei der Codeabdeckung kann es verlockend sein, Fälle auszufüllen, damit die verbleibenden Pfade erreicht werden. Dies gilt insbesondere für nahezu perfekte 98%.
Dies ist das Äquivalent zu einer Rechtschreibprüfung, bei der nur die richtigen Wörter angezeigt werden. Das Ergebnis ist ein Durcheinander.
Wenn Sie sich jedoch überlegen, welche Tests die nicht abgedeckten Pfade wirklich benötigen, hat das Code-Coverage-Tool seine Aufgabe erfüllt. nicht, um Ihnen die Richtigkeit zu versprechen, sondern um auf einige der zu erledigenden Arbeiten hinzuweisen .
quelle
Die Pfadabdeckung kann Ihnen nicht sagen, ob alle erforderlichen Funktionen implementiert wurden. Das Weglassen einer Funktion ist ein Fehler, der jedoch von der Pfadabdeckung nicht erkannt wird.
quelle
Ein Teil des Problems ist, dass 100% ige Abdeckung nur garantiert, dass der Code nach einer einzelnen Ausführung korrekt funktioniert . Einige Fehler, wie z. B. Speicherlecks, sind nach einer einzelnen Ausführung möglicherweise nicht erkennbar oder verursachen Probleme. Mit der Zeit können jedoch Probleme für die Anwendung auftreten.
Angenommen, Sie haben eine Anwendung, die eine Verbindung zu einer Datenbank herstellt. Vielleicht vergisst der Programmierer bei einer Methode, die Verbindung zur Datenbank zu schließen, wenn sie mit ihrer Abfrage fertig sind. Sie könnten mehrere Tests mit dieser Methode durchführen und dabei keine Fehler in der Funktionalität feststellen, aber Ihr Datenbankserver könnte in ein Szenario geraten, in dem keine Verbindungen mehr verfügbar sind, da diese bestimmte Methode die Verbindung nicht geschlossen hat, als sie hergestellt wurde und die Verbindungen geöffnet sein mussten jetzt timeout.
quelle
times_two(x) = x + 2
, wird dies vollständig von der Testsuite abgedecktassert(times_two(2) == 4)
, aber dies ist offensichtlich immer noch fehlerhafter Code! Keine Notwendigkeit für Speicherlecks :)Wie bereits gesagt, lautet die Antwort NEIN.
Abgesehen von dem, was gesagt wird, treten auf verschiedenen Ebenen Fehler auf, die mit Unit-Tests nicht getestet werden können. Um nur einige zu nennen:
quelle
Was bedeutet es für jeden zu testenden Pfad?
Die anderen Antworten sind großartig, aber ich möchte nur hinzufügen, dass die Bedingung "jeder Pfad durch ein Programm wird getestet" selbst vage ist.
Betrachten Sie diese Methode:
Wenn Sie einen Test schreiben, der bestätigt
add(1, 2) == 3
, werden Sie von einem Code-Coverage-Tool darüber informiert, dass jede Zeile ausgeführt wird. Aber Sie haben tatsächlich nichts über den globalen Nebeneffekt oder die nutzlose Zuordnung behauptet. Diese Zeilen wurden ausgeführt, aber nicht wirklich getestet.Mutationstests würden helfen, solche Probleme zu finden. Ein Mutationstest-Tool verfügt über eine Liste vordefinierter Methoden, um den Code zu "mutieren" und festzustellen, ob die Tests noch erfolgreich sind. Zum Beispiel:
+=
zu ändern-=
. Diese Mutation würde keinen Testfehler verursachen. Dies würde also beweisen, dass Ihr Test keine Aussage über den globalen Nebeneffekt macht.Im Wesentlichen sind Mutationstests eine Möglichkeit, Ihre Tests zu testen . Aber so wie Sie niemals die tatsächliche Funktion mit jedem möglichen Satz von Eingaben testen werden, werden Sie niemals jede mögliche Mutation ausführen. Auch dies ist also begrenzt.
Jeder Test, den wir machen können, ist eine Heuristik, um auf fehlerfreie Programme umzusteigen. Nichts ist perfekt.
quelle
Naja ... ja eigentlich, wenn jeder Pfad "durch" das Programm getestet wird. Das heißt aber, jeder mögliche Pfad durch den gesamten Raum aller möglichen Zustände, die das Programm haben kann, einschließlich aller Variablen. Selbst für ein sehr einfaches statisch kompiliertes Programm - zum Beispiel einen alten Fortran-Zahlenknacker - ist dies nicht realisierbar, obwohl es zumindest vorstellbar ist: Wenn Sie nur zwei ganzzahlige Variablen haben, haben Sie im Grunde alle Möglichkeiten, Punkte miteinander zu verbinden ein zweidimensionales Gitter; es sieht tatsächlich sehr nach Travelling Salesman aus. Für n solche Variablen haben Sie es mit einem n- dimensionalen Raum zu tun, sodass die Aufgabe für jedes echte Programm völlig untrahierbar ist.
Schlimmer noch: Für ernsthafte Dinge gibt es nicht nur eine feste Anzahl primitiver Variablen, sondern Sie erstellen Variablen in Funktionsaufrufen im Handumdrehen, oder Sie haben Variablen mit variabler Größe ... oder ähnliches, wie dies in einer Turing-vollständigen Sprache möglich ist. Das macht den Staatsraum unendlich dimensioniert und macht alle Hoffnungen auf eine vollständige Abdeckung zunichte, selbst bei absurd leistungsfähigen Testgeräten.
Das heißt ... eigentlich sind die Dinge nicht ganz so trostlos. Es ist möglich, ganze Programme als richtig zu beweisen , aber Sie müssen ein paar Ideen aufgeben.
Erstens: Es ist sehr ratsam, zu einer deklarativen Sprache zu wechseln. Imperative Sprachen waren aus irgendeinem Grund immer die bei weitem beliebtesten, aber die Art und Weise, wie sie Algorithmen mit realen Interaktionen kombinieren, macht es extrem schwierig, überhaupt zu sagen, was Sie mit „richtig“ meinen .
Viel einfacher in rein funktionalen Programmiersprachen: Diese unterscheiden klar zwischen den wirklich interessanten Eigenschaften mathematischer Funktionen und den unscharfen realen Interaktionen, über die man eigentlich nichts sagen kann. Für die Funktionen ist es sehr einfach, „korrektes Verhalten“ anzugeben: Wenn für alle möglichen Eingaben (aus den Argumenttypen) das entsprechende gewünschte Ergebnis ausgegeben wird, verhält sich die Funktion korrekt.
Nun, Sie sagen, das ist immer noch unlösbar. Schließlich ist der Raum aller möglichen Argumente im Allgemeinen auch unendlich-dimensional. Richtig - obwohl für eine einzelne Funktion sogar naive Abdeckungstests Sie weiter führen, als Sie jemals in einem zwingenden Programm erhoffen könnten! Es gibt jedoch ein unglaublich mächtiges Werkzeug, das das Spiel verändert: universelle Quantifizierung / parametrischer Polymorphismus . Grundsätzlich können Sie auf diese Weise Funktionen für sehr allgemeine Datentypen schreiben, mit der Garantie, dass, wenn dies für ein einfaches Beispiel der Daten funktioniert, dies für alle möglichen Eingaben überhaupt funktioniert.
Zumindest theoretisch. Es ist nicht einfach, die richtigen Typen zu finden, die wirklich so allgemein sind, dass Sie dies vollständig beweisen können - normalerweise benötigen Sie eine abhängig geschriebene Sprache , und diese sind in der Regel recht schwierig zu verwenden. Aber das Schreiben in einem funktionalen Stil mit parametrischem Polymorphismus allein erhöht bereits enorm Ihre „Sicherheitsstufe“ - Sie werden nicht unbedingt alle Fehler finden, aber Sie müssen sie ziemlich gut verbergen, damit der Compiler sie nicht erkennt!
quelle