Wie testet man Code mit Hilfe von Graphstrukturen?


18

Ich schreibe (rekursiven) Code, der in einem Abhängigkeitsdiagramm navigiert und nach Zyklen oder Widersprüchen in den Abhängigkeiten sucht. Ich bin mir jedoch nicht sicher, wie ich mit Unit-Tests umgehen soll. Das Problem ist, dass eines unserer Hauptprobleme darin besteht, dass der Code alle interessanten Diagrammstrukturen verarbeitet und sicherstellt, dass alle Knoten entsprechend behandelt werden.

Während in der Regel 100% Linien- oder Zweigabdeckung ausreichen, um sicher zu sein, dass ein Teil des Codes funktioniert, haben Sie trotz 100% Pfadabdeckung immer noch Zweifel.

Wie wählt man also Diagrammstrukturen für Testfälle aus, um sicherzugehen, dass ihr Code alle denkbaren Permutationen verarbeiten kann, die man in realen Daten findet?


PS: Wenn es darauf ankommt, sind alle Kanten in meinem Diagramm mit "muss haben" oder "kann nicht haben" gekennzeichnet und es gibt keine trivialen Zyklen, und es gibt nur eine Kante zwischen zwei Knoten.


PPS- Diese zusätzliche Problemstellung wurde ursprünglich vom Autor der Frage in einem Kommentar unten veröffentlicht:

For all vertices N in forest F, for all vertices M, in F, such that if there are any walks between N and M they all must either use only edges labelled 'conflict' or 'requires'.


13
Genauso wie Sie eine andere Methode testen. Sie identifizieren alle "interessanten" Testfälle für jede Methode und schreiben Unit-Tests für sie. In Ihrem Fall müssen Sie für jede der "interessanten" Diagrammstrukturen vordefinierte Abhängigkeitsdiagramme erstellen.
Dunk

@Dunk Wir denken immer wieder, dass wir alle kniffligen Probleme haben und stellen dann fest, dass eine bestimmte Struktur Probleme verursacht, an die wir vorher nicht gedacht hatten. Jede knifflige testen , dass wir denken kann ist , was wir tun, was ich zu finden hoffen , ist einige Richtlinien / Verfahren zur Erzeugung von lästigen vielleicht Beispiele Reduzierbarkeit von Grundformen unter Verwendung usw.
Schlitten

6
Das ist das Problem bei jeder Form von Tests. Sie wissen nur, dass die Tests, an die Sie gedacht haben, funktionieren. Es bedeutet nicht, dass Ihr SW fehlerfrei ist, nur weil Ihre Tests bestanden wurden. Jedes Projekt hat das gleiche Problem. Ich bin in der Endphase der Auslieferung meines aktuellen Projekts, damit wir mit der Fertigung beginnen können. Die Arten von Fehlern, auf die wir jetzt stoßen, sind eher dunkel. Zum Beispiel, wenn die Hardware immer noch den Spezifikationen entspricht, aber kaum und wenn sie mit anderer Hardware gleichzeitig mit demselben Problem kombiniert wird, treten Probleme auf. aber nur manchmal :( Die sw ist gut getestet, aber wir haben nicht an alles gedacht
Dunk

Was Sie beschreiben, klingt eher nach Integrationstests und nicht nach Unit-Tests. Unit-Tests würden sicherstellen, dass eine Methode die Kreise in einem Diagramm finden kann. Andere Komponententests würden sicherstellen, dass ein bestimmter Kreis eines bestimmten Graphen von der zu testenden Klasse behandelt wird.
SpaceTrucker

Die Zykluserkennung ist ein ausführliches Thema (siehe Knuth und einige Antworten weiter unten), und die Lösungen umfassen nicht viele Sonderfälle. Daher sollten Sie zunächst ermitteln, wie sich Ihr Problem auf diese Weise auswirkt. Liegt es an den von Ihnen genannten Widersprüchen? In diesem Fall benötigen wir weitere Informationen. Wenn es sich um ein Ergebnis von Implementierungsentscheidungen handelt, müssen Sie möglicherweise große Umgestaltungen vornehmen. Grundsätzlich ist dies ein Designproblem, das Sie durchdenken müssen. TDD ist der falsche Ansatz, der Sie tief in das Labyrinth führen kann, bevor Sie in eine Sackgasse geraten.
Sdenham

Antworten:


5

Wir denken immer wieder, dass wir alle kniffligen Probleme haben, und dann stellen wir fest, dass eine bestimmte Struktur Probleme verursacht, an die wir vorher nicht gedacht hatten. Wir testen jeden Trick, den wir uns vorstellen können.

Das klingt nach einem guten Start. Ich vermute, Sie haben bereits versucht, einige klassische Techniken wie die Grenzwertanalyse oder die Äquivalenzpartitionierung anzuwenden , wie Sie bereits erwähnt haben. Nachdem Sie viel Zeit in die Erstellung guter Testfälle investiert haben, werden Sie zu einem Punkt kommen, an dem Ihnen, Ihrem Team und auch Ihren Testern (falls Sie diese haben) die Ideen ausgehen. Und an diesem Punkt sollten Sie den Pfad des Unit- Testens verlassen und mit möglichst vielen Daten aus der realen Welt beginnen.

Es sollte offensichtlich sein, dass Sie versuchen sollten, eine große Vielfalt von Diagrammen aus Ihren Produktionsdaten auszuwählen. Möglicherweise müssen Sie einige zusätzliche Tools oder Programme nur für diesen Teil des Prozesses schreiben. Das Schwierigste dabei ist wahrscheinlich, die Richtigkeit der Programmausgabe zu überprüfen. Wenn Sie zehntausende verschiedene Diagramme aus der realen Welt in Ihr Programm einfügen, wie können Sie dann feststellen, ob Ihr Programm immer die richtige Ausgabe liefert? Offensichtlich können Sie nicht als manuell überprüfen. Wenn Sie Glück haben, können Sie möglicherweise eine zweite, sehr einfache Implementierung Ihrer Abhängigkeitsprüfung durchführen, die Ihre Leistungserwartungen möglicherweise nicht erfüllt, aber einfacher zu überprüfen ist als Ihr ursprünglicher Algorithmus. Sie sollten auch versuchen, viele Plausibilitätsprüfungen direkt in Ihr Programm zu integrieren (z. B.

Lernen Sie schließlich zu akzeptieren, dass jeder Test nur das Vorhandensein von Fehlern beweisen kann, nicht aber das Fehlen von Fehlern.


5

1. Randomisierte Testgenerierung

Schreiben Sie einen Algorithmus, der Graphen erzeugt, lassen Sie ihn ein paar hundert (oder mehr) zufällige Graphen erzeugen und werfen Sie jeden auf Ihren Algorithmus.

Behalten Sie die Zufallszahl der Diagramme bei, die interessante Fehler verursachen, und fügen Sie diese als Komponententests hinzu.

2. Schwierige Teile mit hartem Code

Einige Diagrammstrukturen, von denen Sie wissen, dass sie schwierig sind, können Sie sofort einprogrammieren oder Code schreiben, der sie kombiniert und an Ihren Algorithmus weiterleitet.

3. Vollständige Liste erstellen

Wenn Sie jedoch sicher sein möchten, dass "der Code alle denkbaren Permutationen verarbeiten kann, die Sie in Daten der realen Welt finden", müssen Sie diese Daten nicht aus zufälligen Ausgangswerten generieren, sondern indem Sie alle Permutationen durchlaufen. (Dies wird beim Testen von U-Bahn-Signalsystemen durchgeführt und bietet eine enorme Anzahl von Fällen, deren Prüfung Ewigkeiten in Anspruch nimmt. Bei U-Bahnen ist das System beschränkt, sodass die Anzahl der Permutationen nach oben begrenzt ist. Sie sind sich nicht sicher, wie Ihr Fall lautet.) gilt)


Der Fragesteller hat geschrieben, dass er nicht sagen kann, ob er alle Fälle berücksichtigt hat, was impliziert, dass er keine Möglichkeit hat, sie aufzuzählen. Bis sie die Problemdomäne gut genug verstehen, um dies zu tun, ist das Testen eine strittige Frage.
Sdenham

@sdenham Wie wollen Sie etwas aufzählen, das eine unendliche Anzahl von möglichen gültigen Kombinationen hat? Ich hatte gehofft, etwas in der Art von "Dies sind die schwierigsten Graphstrukturen, die häufig Fehler in Ihrer Implementierung aufdecken" zu finden. Ich verstehe die Domain gut genug, da es einfach ist: For all vertices N in forest F, for all vertices M, in F, such that if there are any walks between N and M they all must either use only edges labelled 'conflict' or 'requires'.Die Domain ist nicht das Problem.
Schlitten

@ArtB: Vielen Dank für Ihre Klärung des Problems. Wie Sie gesagt haben, gibt es zwischen zwei Eckpunkten nicht mehr als eine Kante, und Pfade mit Zyklen sind anscheinend ausgeschlossen (oder mindestens ein Durchlauf um einen Zyklus), dann wissen wir zumindest, dass es nicht buchstäblich eine unendliche Anzahl von gibt mögliche gültige kombinationen, das ist fortschritt. Beachten Sie, dass es nicht gleichbedeutend ist, zu wissen, wie alle Möglichkeiten aufgezählt werden, wie Sie es tun müssen, da dies ein Ausgangspunkt für ein Argument für die Richtigkeit sein kann, das wiederum den Test leiten kann. Ich werde mehr darüber nachdenken ...
Sdenham

@ArtB: Sie sollten die Frage so ändern, dass sie die Aktualisierung der hier angegebenen Problemstellung enthält. Es kann auch hilfreich sein, festzustellen, ob es sich um gerichtete Kanten handelt (falls dies der Fall ist) und ob ein Zyklus als Fehler im Diagramm und nicht nur als eine vom Algorithmus zu behandelnde Situation angesehen wird.
Sdenham

4

In diesem Fall werden keinerlei Tests ausreichen, nicht einmal Tonnen von Daten aus der realen Welt oder Fuzzing. Eine 100% ige Codeabdeckung oder sogar eine 100% ige Pfadabdeckung reicht nicht aus, um rekursive Funktionen zu testen.

Entweder hält die rekursive Funktion einem formalen Beweis stand (sollte in diesem Fall nicht so schwierig sein) oder nicht. Wenn der Code zu stark mit anwendungsspezifischem Code verflochten ist, um Nebenwirkungen auszuschließen, sollten Sie damit beginnen.

Der Algorithmus selbst klingt wie ein einfacher Überflutungsalgorithmus, ähnlich einer einfachen umfassenden ersten Suche, mit der Hinzufügung einer schwarzen Liste, die sich nicht mit der Liste der besuchten Knoten überschneiden darf, die von allen Knoten ausgeführt werden.

foreach nodes as node
    foreach nodes as tmp
        tmp.status = unmarked

    tovisit = []
    tovisit.push(node)
    node.status = required

    while |tovisit| > 0 do
        next = tovisit.pop()
        foreach next.requires as requirement
            if requirement.status = unmarked
                tovisit.push(requirement)
                requirement.status = required
            else if requirement.status = blacklisted
                return false
        foreach next.collides as collision
            if collision.status = unmarked
                requirement.status = blacklisted
            else if requirement.status = required
                return false
return true

Dieser iterative Algorithmus erfüllt die Bedingung, dass für Graphen mit beliebiger Struktur ausgehend von einem beliebigen Artefakt keine Abhängigkeit erforderlich und gleichzeitig auf die schwarze Liste gesetzt werden darf, wobei das Startartefakt immer erforderlich ist.

Es kann zwar so schnell wie Ihre eigene Implementierung sein oder auch nicht, es kann jedoch bewiesen werden, dass es für alle Fälle endet (da für jede Iteration der äußeren Schleife jedes Element nur einmal in die tovisitWarteschlange verschoben werden kann), es überschwemmt das gesamte erreichbare Element Es erkennt alle Fälle, in denen ein Artefakt gleichzeitig benötigt und auf die schwarze Liste gesetzt werden muss, beginnend bei jedem Knoten.

Wenn Sie nachweisen können, dass Ihre eigene Implementierung dieselben Merkmale aufweist, können Sie die Richtigkeit nachweisen, ohne dass dies zu einem Komponententest führt. Es müssen nur die grundlegenden Methoden zum Pushen und Poppen aus Warteschlangen, Zählen der Warteschlangenlänge, Durchlaufen von Eigenschaften usw. getestet werden, und es muss nachgewiesen werden, dass sie frei von Nebenwirkungen sind.

BEARBEITEN: Was dieser Algorithmus nicht beweist, ist, dass Ihr Graph frei von Zyklen ist. Gerichtete azyklische Graphen sind jedoch ein gut recherchiertes Thema, daher sollte es auch einfach sein, einen vorgefertigten Algorithmus zum Nachweis dieser Eigenschaft zu finden.

Wie Sie sehen, muss das Rad keinesfalls neu erfunden werden.


3

Sie verwenden Ausdrücke wie "alle interessanten Diagrammstrukturen" und "entsprechend behandelt". Sofern Sie nicht die Möglichkeit haben, Ihren Code anhand all dieser Strukturen zu testen und festzustellen, ob der Code das Diagramm ordnungsgemäß verarbeitet, können Sie nur Tools wie die Testabdeckungsanalyse verwenden.

Ich schlage vor, Sie beginnen mit dem Auffinden und Testen einer Reihe interessanter Diagrammstrukturen, ermitteln die geeignete Behandlung und stellen fest, dass der Code dies tut. Dann können Sie anfangen, diese Graphen in a) fehlerhafte Graphen umzuwandeln, die gegen Regeln verstoßen, oder b) nicht so interessante Graphen, die Probleme haben; Überprüfen Sie, ob Ihr Code diese nicht ordnungsgemäß verarbeitet.


Dies ist zwar ein guter Ansatz zum Testen, löst jedoch nicht das zentrale Problem der Frage: Wie kann sichergestellt werden, dass alle Fälle abgedeckt sind? Das erfordert meiner Meinung nach weitere Analysen und möglicherweise ein Refactoring - siehe meine Frage oben.
Sdenham


2

Wenn es um diese Art von schwer zu testendem Algorithmus geht, würde ich mich für das TDD entscheiden, bei dem Sie den Algorithmus basierend auf Tests erstellen.

TDD kurz gesagt,

  • schreibe den Test
  • sehe, es scheitert
  • Ändern Sie den Code
  • Stellen Sie sicher, dass alle Tests bestanden werden
  • Refactor

und den Zyklus wiederholen,

In dieser besonderen Situation,

  1. Der erste Test wäre ein Graph mit einem einzelnen Knoten, bei dem der Algorithmus keine Zyklen zurückgeben sollte
  2. Der zweite wäre ein Drei-Knoten-Graph ohne Zyklus, wobei der Algorithmus keine Zyklen zurückgeben sollte
  3. Als nächstes würde man einen Drei-Knoten-Graphen mit einem Zyklus verwenden, bei dem der Algorithmus keine Zyklen zurückgeben sollte
  4. Jetzt können Sie es je nach Möglichkeit gegen einen etwas komplexeren Zyklus testen

Ein wichtiger Aspekt bei dieser Methode ist, dass Sie immer einen Test für einen möglichen Schritt hinzufügen müssen (bei dem Sie mögliche Szenarien in einfache Tests aufteilen). Wenn Sie alle möglichen Szenarien abdecken, wird der Algorithmus normalerweise automatisch entwickelt.

Schließlich müssen Sie einen oder mehrere komplizierte Integrationstests hinzufügen, um zu ermitteln, ob unvorhergesehene Probleme vorliegen (z. B. Stapelüberlauffehler / Leistungsfehler, wenn Ihr Diagramm sehr groß ist und Sie die Rekursion verwenden).


2

Mein Verständnis des Problems, wie es ursprünglich angegeben und dann durch Kommentare unter Mackes Antwort aktualisiert wurde, umfasst Folgendes: 1) Beide Kantentypen (Abhängigkeiten und Konflikte) sind gerichtet. 2) wenn zwei Knoten durch eine Kante verbunden sind, dürfen sie nicht durch eine andere verbunden werden, auch wenn es sich um einen anderen Typ handelt oder umgekehrt; 3) Wenn ein Pfad zwischen zwei Knoten konstruiert werden kann, indem Kanten verschiedener Typen gemischt werden, ist dies eher ein Fehler als ein Umstand, der ignoriert wird. 4) Wenn es einen Pfad zwischen zwei Knoten gibt, die Kanten eines Typs verwenden, gibt es möglicherweise keinen anderen Pfad zwischen ihnen, wenn Kanten des anderen Typs verwendet werden. 5) Zyklen eines einzelnen Kantentyps oder gemischter Kantentypen sind nicht zulässig (nach einer Schätzung der Anwendung bin ich nicht sicher, ob Konfliktzyklen ein Fehler sind, aber diese Bedingung kann entfernt werden, wenn dies nicht der Fall ist.)

Weiterhin gehe ich davon aus, dass die verwendete Datenstruktur Verstöße gegen diese Anforderungen nicht verhindert (zum Beispiel könnte ein Graph, der die Bedingung 2 verletzt, nicht in einer Abbildung von Knotenpaar zu (Typ, Richtung) ausgedrückt werden, wenn das Knotenpaar immer hat zuerst den Knoten mit der niedrigsten Nummer.) Wenn bestimmte Fehler nicht ausgedrückt werden können, wird die Anzahl der zu berücksichtigenden Fälle verringert.

Tatsächlich gibt es drei Diagramme, die hier betrachtet werden können: die zwei mit ausschließlich einem Kantentyp und das gemischte Diagramm, das durch die Vereinigung von einem der beiden Typen gebildet wird. Auf diese Weise können Sie systematisch alle Diagramme bis zu einer bestimmten Anzahl von Knoten erstellen. Erzeugen Sie zunächst alle möglichen Graphen von N Knoten mit nicht mehr als einer Kante zwischen zwei geordneten Knotenpaaren (geordnete Paare, da es sich um gerichtete Graphen handelt). Nehmen Sie nun alle möglichen Paare dieser Graphen, von denen eines Abhängigkeiten und das andere Konflikte darstellt, und bilden die Vereinigung jedes Paares.

Wenn Ihre Datenstruktur Verstöße gegen Bedingung 2 nicht ausdrücken kann, können Sie die zu berücksichtigenden Fälle erheblich reduzieren, indem Sie nur alle möglichen Konfliktdiagramme erstellen, die in die Bereiche der Abhängigkeitsdiagramme passen, oder umgekehrt. Andernfalls können Sie Verstöße gegen Bedingung 2 beim Bilden der Vereinigung erkennen.

Beim Durchlaufen des kombinierten Graphen vom ersten Knoten an können Sie die Menge aller Pfade zu jedem erreichbaren Knoten erstellen und dabei nach Verstößen gegen alle Bedingungen suchen (für die Zykluserkennung können Sie dies tun) benutze Tarjans Algorithmus .)

Sie müssen nur Pfade vom ersten Knoten berücksichtigen, auch wenn das Diagramm nicht verbunden ist, da Pfade von jedem anderen Knoten in einem anderen Fall als Pfade vom ersten Knoten angezeigt werden.

Wenn Pfade mit gemischten Kanten einfach ignoriert werden können, anstatt Fehler zu sein (Bedingung 3), ist es ausreichend, die Abhängigkeits- und Konfliktdiagramme unabhängig voneinander zu betrachten und zu überprüfen, ob ein Knoten in einem erreichbar ist, der andere nicht.

Wenn Sie sich an die Pfade erinnern, die beim Untersuchen von Diagrammen von N-1-Knoten gefunden wurden, können Sie diese als Ausgangspunkt für das Generieren und Auswerten von Diagrammen von N-Knoten verwenden.

Dadurch werden nicht mehrere Kanten desselben Typs zwischen Knoten generiert, es kann jedoch eine Erweiterung vorgenommen werden. Dies würde jedoch die Anzahl der Fälle stark erhöhen. Es wäre daher besser, wenn der getestete Code es unmöglich machen würde, alle derartigen Fälle im Voraus darzustellen, oder wenn dies nicht der Fall wäre.

Der Schlüssel zum Schreiben eines solchen Orakels besteht darin, es so einfach wie möglich zu halten, auch wenn dies ineffizient bedeutet, damit Sie Vertrauen in es aufbauen können (idealerweise durch Argumente für seine Richtigkeit, die durch Tests gestützt werden).

Sobald Sie über die Mittel verfügen, um Testfälle zu generieren, und Sie darauf vertrauen, dass das von Ihnen erstellte Orakel die guten von den schlechten trennt, können Sie damit das automatisierte Testen des Zielcodes vorantreiben. Wenn dies nicht möglich ist, können Sie die Ergebnisse am besten nach Einzelfällen durchsuchen. Das Orakel kann die gefundenen Fehler klassifizieren und Ihnen einige Informationen zu den akzeptierten Fällen geben, z. B. die Anzahl und Länge der Pfade jedes Typs und ob sich am Anfang beider Pfadtypen Knoten befinden könnte Ihnen helfen, nach Fällen zu suchen, die Sie vorher nicht gesehen haben.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.