Wie schreibt man Komponententests für Code mit schwer vorhersagbaren Ergebnissen?

124

Ich arbeite häufig mit sehr numerischen / mathematischen Programmen, bei denen es schwierig ist, das genaue Ergebnis einer Funktion vorherzusagen.

Bei dem Versuch, TDD mit dieser Art von Code anzuwenden, ist das Schreiben des zu testenden Codes häufig wesentlich einfacher als das Schreiben von Komponententests für diesen Code, da ich das erwartete Ergebnis nur mit der Anwendung des Algorithmus selbst (unabhängig davon, ob in meinem Kopf, auf Papier oder am Computer). Dies fühlt sich falsch an, da ich den getesteten Code effektiv verwende, um meine Komponententests zu verifizieren, anstatt umgekehrt.

Gibt es bekannte Techniken zum Schreiben von Komponententests und zum Anwenden von TDD, wenn das Ergebnis des zu testenden Codes schwer vorherzusagen ist?

Ein (reales) Beispiel für Code mit schwer vorhersagbaren Ergebnissen:

Eine Funktion weightedTasksOnTime, die eine Menge an Arbeit pro Tag getan gegeben workPerDayin Bereich (0, 24], die aktuelle Uhrzeit initialTime> 0, und eine Liste von Aufgaben taskArray, die jeweils mit einer Zeit abzuschließen Eigentum time> 0, Fälligkeit dueund Wichtigkeitswert importance; Renditen Ein normalisierter Wert im Bereich [0, 1], der die Wichtigkeit von Aufgaben darstellt, die vor ihrem dueDatum erledigt werden können, wenn jede Aufgabe in der von angegebenen Reihenfolge erledigt wird taskArray, beginnend mit initialTime.

Der Algorithmus zum Implementieren dieser Funktion ist relativ einfach: Iterieren Sie über Tasks in taskArray. Für jede Aufgabe hinzufügen timezu initialTime. Wenn die neue Zeit <ist due, fügen Sie importancesie einem Akkumulator hinzu. Die Zeit wird durch inverse workPerDay angepasst. Teilen Sie vor der Rückgabe des Akkus die zu normalisierenden Aufgabenbedeutungen durch die Summe.

function weightedTasksOnTime(workPerDay, initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time * (24 / workPerDay)
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator / totalImportance(taskArray)
}

Ich glaube, dass das obige Problem unter Beibehaltung seines Kerns vereinfacht werden kann, indem workPerDaydie Normierungsanforderung beseitigt wird, um Folgendes zu ergeben:

function weightedTasksOnTime(initialTime, taskArray) {
    let simulatedTime = initialTime
    let accumulator = 0;
    for (task in taskArray) {
        simulatedTime += task.time
        if (simulatedTime < task.due) {
            accumulator += task.importance
        }
    }
    return accumulator
}

Diese Frage befasst sich mit Situationen, in denen der zu testende Code keine Neuimplementierung eines vorhandenen Algorithmus ist. Wenn es sich bei Code um eine Neuimplementierung handelt, lassen sich die Ergebnisse von sich aus leicht vorhersagen, da vorhandene vertrauenswürdige Implementierungen des Algorithmus als natürliches Testorakel fungieren.

PaintingInAir
quelle
4
Können Sie ein einfaches Beispiel für eine Funktion angeben, deren Ergebnis schwer vorherzusagen ist?
Robert Harvey
62
FWIW Sie testen den Algorithmus nicht. Vermutlich ist das richtig. Sie testen die Implementierung. Das Ausarbeiten von Hand ist oft als Parallelkonstruktion gut geeignet.
Kristian H
7
Es gibt Situationen, in denen ein Algorithmus nicht zuverlässig auf Einheit getestet werden kann - zum Beispiel, wenn seine Ausführungszeit mehrere Tage / Monate beträgt. Dies kann bei der Lösung von NP-Problemen vorkommen. In diesen Fällen kann es praktikabler sein, einen formellen Nachweis für die Richtigkeit des Codes zu erbringen .
Hulk
12
Ich habe in einem sehr kniffligen numerischen Code gesehen, dass Unit-Tests nur als Regressionstests behandelt werden. Schreiben Sie die Funktion, führen Sie sie für einige interessante Werte aus, validieren Sie die Ergebnisse manuell und schreiben Sie dann den Komponententest, um Regressionen vom erwarteten Ergebnis aufzufangen. Horror codieren? Neugierig was andere denken.
Chuu

Antworten:

251

Es gibt zwei Dinge, die Sie in schwer zu testendem Code testen können. Erstens die entarteten Fälle. Was passiert, wenn sich in Ihrem Task-Array keine oder nur eines oder zwei Elemente befinden, aber eines nach dem Fälligkeitsdatum liegt usw. Alles, was einfacher als Ihr eigentliches Problem ist, aber dennoch vernünftig ist, manuell zu berechnen.

Der zweite ist die Sanity Checks. Dies sind die Prüfungen, bei denen Sie nicht wissen, ob eine Antwort richtig ist , aber auf jeden Fall wissen, ob sie falsch ist . Dies sind Dinge wie Zeit muss voranschreiten, Werte müssen in einem vernünftigen Bereich liegen, Prozentsätze müssen sich zu 100 addieren usw.

Ja, dies ist nicht so gut wie ein vollständiger Test, aber Sie werden überrascht sein, wie oft Sie die Überprüfung der geistigen Gesundheit und entartete Fälle durcheinander bringen, was ein Problem in Ihrem vollständigen Algorithmus aufdeckt.

Karl Bielefeldt
quelle
54
Ich denke, das ist ein sehr guter Rat. Beginnen Sie mit dem Schreiben dieser Art von Komponententests. Wenn Sie bei der Entwicklung der Software Fehler oder falsche Antworten finden, fügen Sie diese als Komponententests hinzu. Tun Sie dasselbe, bis zu einem gewissen Grad, wenn Sie definitiv richtige Antworten finden. Bauen Sie sie im Laufe der Zeit auf, und Sie werden (irgendwann) einen kompletten Satz von Unit-Tests haben, obwohl Sie nicht wissen, wie sie
aussehen würden
21
Eine andere Sache, die in manchen Fällen hilfreich sein kann (wenn auch nicht diese), ist das Schreiben einer inversen Funktion und das Testen, dass Ihre Eingabe und Ausgabe, wenn sie verkettet sind, identisch sind.
Cyberspark
7
Sanity Checks sind oft gute Ziele für immobilienbasierte Tests mit so etwas wie QuickCheck
jk.
10
Die eine andere Kategorie von Tests, die ich empfehlen würde, sind einige, die auf unbeabsichtigte Änderungen der Ausgabe überprüft werden sollen. Sie können diese "betrügen", indem Sie den Code selbst verwenden, um das erwartete Ergebnis zu generieren, da dies den Betreuern helfen soll, indem Sie markieren, dass etwas, das als ausgangsneutrale Änderung gedacht ist, unbeabsichtigt das algorithmische Verhalten beeinflusst hat.
Dan Neely
5
@iFlo Ich bin mir nicht sicher, ob Sie Witze gemacht haben, aber die inverse Inverse existiert bereits. Es lohnt sich zu bemerken, dass das
Fehlschlagen
80

Früher habe ich Tests für wissenschaftliche Software mit schwer vorhersehbaren Ergebnissen geschrieben. Wir haben viel von metamorphen Beziehungen Gebrauch gemacht. Grundsätzlich wissen Sie, wie sich Ihre Software verhalten soll, auch wenn Sie keine genauen numerischen Ausgaben kennen.

Ein mögliches Beispiel für Ihren Fall: Wenn Sie die Menge der Arbeit, die Sie täglich erledigen können, verringern, bleibt die Gesamtmenge der Arbeit, die Sie erledigen können, bestenfalls gleich, nimmt jedoch wahrscheinlich ab. Führen Sie die Funktion für eine Reihe von Werten von aus workPerDayund stellen Sie sicher, dass die Beziehung gültig ist.

James Elderfield
quelle
32
Metamorphe Beziehungen ein spezifisches Beispiel für Eigenschafts-basiertes Testen , das im Allgemeinen ein nützliches Werkzeug für Situationen wie diese ist
Dannnno
38

Die anderen Antworten haben gute Ideen für die Entwicklung von Tests für Kanten- oder Fehlerfälle. Für die anderen ist die Verwendung des Algorithmus selbst nicht ideal (offensichtlich), aber dennoch nützlich.

Es erkennt, ob sich der Algorithmus (oder die Daten, von denen er abhängt) geändert hat

Wenn die Änderung ein Unfall ist, können Sie ein Commit zurücksetzen. Wenn die Änderung beabsichtigt war, müssen Sie den Komponententest erneut durchführen.

user949300
quelle
6
Und für die Aufzeichnung, diese Art von Tests werden oft als "Regressionstests" bezeichnet und sind im Grunde genommen ein Sicherheitsnetz für jede Änderung / Umgestaltung.
Pac0
21

So schreiben Sie Unit-Tests für jede andere Art von Code:

  1. Finden Sie einige repräsentative Testfälle und testen Sie diese.
  2. Finden Sie Edge Cases und testen Sie diese.
  3. Finden Sie Fehlerbedingungen und testen Sie diese.

Sofern Ihr Code kein zufälliges Element enthält oder nicht deterministisch ist (dh nicht dieselbe Ausgabe bei derselben Eingabe erzeugt), ist er einheitentestbar.

Vermeiden Sie Nebenwirkungen oder Funktionen, die von äußeren Kräften beeinflusst werden. Reine Funktionen sind einfacher zu testen.

Robert Harvey
quelle
2
Für nicht-deterministische Algorithmen können Sie Samen von RNG speichern oder verspotten sie entweder mit fester Sequenz oder niedrige Diskrepanz determinitistic Serie mit zB Halton - Sequenz
Wondra
14
@PaintingInAir Wenn es unmöglich ist , den Algorithmus ausgegeben , um zu überprüfen, kann der Algorithmus auch sein nicht richtig?
WolfgangGroiss
5
Unless your code involves some random elementDer Trick dabei ist, Ihren Zufallszahlengenerator zu einer injizierten Abhängigkeit zu machen, sodass Sie ihn dann durch einen Zahlengenerator ersetzen können, der genau das Ergebnis liefert, das Sie möchten. Auf diese Weise können Sie erneut genau testen und die generierten Zahlen auch als Eingabeparameter zählen. not deterministic (i.e. it won't produce the same output given the same input)Da ein Komponententest von einer kontrollierten Situation ausgehen sollte, kann er nur dann nicht deterministisch sein, wenn er ein zufälliges Element enthält, das Sie dann injizieren können. Ich kann mir hier keine anderen Möglichkeiten vorstellen.
Flater
3
@PaintingInAir: Entweder oder. Mein Kommentar gilt sowohl für die schnelle Ausführung als auch für das schnelle Schreiben von Tests. Wenn Sie drei Tage benötigen, um ein einzelnes Beispiel von Hand zu berechnen (nehmen wir an, Sie verwenden die schnellste verfügbare Methode, bei der der Code nicht verwendet wird), sind es drei Tage. Wenn Sie Ihr erwartetes Testergebnis stattdessen auf dem tatsächlichen Code selbst basieren, gefährdet sich der Test von selbst. Das ist so if(x == x), als wäre es ein sinnloser Vergleich. Sie müssen Ihre beiden Ergebnisse ( tatsächlich : stammt aus dem Code; erwartet : stammt aus Ihrem externen Wissen) voneinander unabhängig sein.
Flater
2
Es ist immer noch einheitenprüfbar, auch wenn es nicht deterministisch ist, vorausgesetzt, es entspricht den Spezifikationen und die Einhaltung kann gemessen werden (z. B. Verteilung und Streuung nach dem Zufallsprinzip). Es sind möglicherweise nur sehr viele Proben erforderlich, um das Risiko einer Anomalie auszuschließen.
McKenzm
17

Update aufgrund von geposteten Kommentaren

Die ursprüngliche Antwort wurde aus Gründen der Kürze entfernt. Sie finden sie im Bearbeitungsverlauf.

PaintingInAir Für den Kontext: Als Unternehmer und Akademiker werden die meisten Algorithmen, die ich entwerfe, von niemand anderem als mir selbst angefordert. Das in der Frage angegebene Beispiel ist Teil eines ableitungsfreien Optimierers, um die Qualität einer Reihenfolge von Aufgaben zu maximieren. In Bezug darauf, wie ich die Notwendigkeit der Beispielfunktion intern beschrieben habe: "Ich benötige eine objektive Funktion, um die Wichtigkeit von Aufgaben zu maximieren, die rechtzeitig erledigt werden." Es scheint jedoch immer noch eine große Lücke zwischen dieser Forderung und der Durchführung von Komponententests zu bestehen.

Erstens ein TL; DR , um eine sonst lange Antwort zu vermeiden:

Stellen Sie sich das so vor:
Ein Kunde betritt McDonald's und bittet um einen Burger mit Salat, Tomaten und Handseife als Belag. Diese Bestellung erhält der Koch, der den Burger genau nach Wunsch zubereitet. Der Kunde erhält diesen Burger, isst ihn und beschwert sich dann beim Koch, dass dies kein leckerer Burger ist!

Dies ist nicht die Schuld des Kochs - er tut nur, was der Kunde ausdrücklich verlangt. Es ist nicht die Aufgabe des Kochs, zu überprüfen, ob die angeforderte Bestellung wirklich lecker ist . Der Koch kreiert einfach das, was der Kunde bestellt. Es liegt in der Verantwortung des Kunden, etwas zu bestellen, das ihm schmeckt .

Ebenso ist es nicht die Aufgabe des Entwicklers, die Richtigkeit des Algorithmus in Frage zu stellen. Ihre einzige Aufgabe ist es, den Algorithmus wie gewünscht zu implementieren.
Unit Testing ist ein Tool für Entwickler. Es bestätigt, dass der Burger der Bestellung entspricht (bevor er die Küche verlässt). Es wird nicht (und sollte nicht) versucht zu bestätigen, dass der bestellte Burger tatsächlich lecker ist.

Selbst wenn Sie sowohl Kunde als auch Koch sind, gibt es immer noch eine sinnvolle Unterscheidung zwischen:

  • Ich habe dieses Essen nicht richtig zubereitet, es war nicht lecker (= Kochfehler). Ein gebranntes Steak wird niemals gut schmecken, auch wenn Sie Steak mögen.
  • Ich habe das Essen richtig zubereitet, aber es gefällt mir nicht (= Kundenfehler). Wenn Sie kein Steak mögen, werden Sie nie gerne Steak essen, auch wenn Sie es perfekt zubereitet haben.

Das Hauptproblem hierbei ist, dass Sie keine Trennung zwischen dem Kunden und dem Entwickler (und dem Analysten - obwohl diese Rolle auch von einem Entwickler vertreten werden kann) vornehmen.

Sie müssen zwischen dem Testen des Codes und dem Testen der Geschäftsanforderungen unterscheiden.

Zum Beispiel möchte der Kunde es so arbeiten [dieser] . Allerdings verkennt der Entwickler, und er schreibt Code, der tut , [die] .

Der Entwickler schreibt daher Komponententests, die prüfen, ob [das] wie erwartet funktioniert. Wenn er die Anwendung richtig entwickelt, wird seine Einheit Tests bestehen , auch wenn die Anwendung nicht tut , [diese] , die der Kunde es erwartet hatte.

Wenn Sie die Erwartungen des Kunden (die Geschäftsanforderungen) testen möchten, muss dies in einem separaten (und späteren) Schritt erfolgen.

Ein einfacher Entwicklungsworkflow, der Ihnen zeigt, wann diese Tests ausgeführt werden sollten:

  • Der Kunde erklärt das zu lösende Problem.
  • Der Analyst (oder Entwickler) schreibt dies in eine Analyse.
  • Der Entwickler schreibt Code, der das tut, was in der Analyse beschrieben wird.
  • Der Entwickler testet seinen Code (Komponententests), um festzustellen, ob er die Analyse korrekt durchgeführt hat
  • Wenn die Komponententests fehlschlagen, kehrt der Entwickler zur Entwicklung zurück. Dies wird auf unbestimmte Zeit wiederholt, bis das Gerät alle Tests bestanden hat.
  • Mit einer getesteten (bestätigten und bestandenen) Codebasis erstellt der Entwickler die Anwendung.
  • Die Bewerbung wird dem Kunden übergeben.
  • Der Kunde prüft nun , ob die von ihm angegebene Anwendung das von ihm gesuchte Problem tatsächlich löst (QS-Tests) .

Sie fragen sich vielleicht, warum es wichtig ist, zwei separate Tests durchzuführen, wenn Kunde und Entwickler ein und dasselbe sind. Da es keine "Übergabe" vom Entwickler an den Kunden gibt, werden die Tests nacheinander ausgeführt, es handelt sich jedoch immer noch um separate Schritte.

  • Unit-Tests sind ein spezielles Tool, mit dem Sie überprüfen können, ob Ihre Entwicklungsphase abgeschlossen ist.
  • QS-Tests werden mithilfe der Anwendung durchgeführt .

Wenn Sie testen möchten, ob Ihr Algorithmus selbst korrekt ist , gehört dies nicht zur Aufgabe des Entwicklers . Dies ist Sache des Kunden, und der Kunde wird dies anhand der Anwendung testen .

Als Unternehmer und Akademiker fehlt Ihnen hier möglicherweise eine wichtige Unterscheidung, die die unterschiedlichen Verantwortlichkeiten verdeutlicht.

  • Wenn sich die Anwendung nicht an die ursprüngliche Aufforderung des Kunden hält, werden die nachfolgenden Änderungen des Codes in der Regel kostenlos durchgeführt . da es ein Entwicklerfehler ist. Der Entwickler hat einen Fehler gemacht und muss die Kosten für die Fehlerbehebung tragen.
  • Wenn die Anwendung das tut, worum der Kunde ursprünglich gebeten hatte, der Kunde jedoch seine Meinung geändert hat (z. B. wenn Sie sich für einen anderen und besseren Algorithmus entschieden haben), werden die Änderungen an der Codebasis dem Kunden in Rechnung gestellt , da dies nicht der Fall ist Entwickler schuld daran, dass der Kunde nach etwas anderem gefragt hat, als er jetzt möchte. Es liegt in der Verantwortung des Kunden (Kosten), seine Meinung zu ändern, und daher müssen die Entwickler mehr Anstrengungen unternehmen, um etwas zu entwickeln, das zuvor nicht vereinbart wurde.
Flater
quelle
Ich würde mich freuen, wenn die Situation "Wenn Sie sich den Algorithmus ausgedacht haben" ausführlicher erörtert würde, da ich denke, dass dies die Situation ist, die am wahrscheinlichsten Probleme aufwirft. Insbesondere in Situationen, in denen keine Beispiele für "Wenn A dann B, Andernfalls C" bereitgestellt werden. (ps Ich bin nicht der Downvoter)
PaintingInAir
@PaintingInAir: Aber darauf kann ich nicht näher eingehen, da dies von Ihrer Situation abhängt. Wenn Sie sich entschieden haben, diesen Algorithmus zu erstellen, haben Sie dies offensichtlich getan, um eine bestimmte Funktion bereitzustellen. Wer hat dich darum gebeten? Wie haben sie ihre Anfrage beschrieben? Haben sie dir gesagt, was in bestimmten Szenarien passieren musste? (Diese Information ist das, was ich in meiner Antwort als "die Analyse" bezeichne.) Jede Erklärung, die Sie erhalten haben (die Sie zum Erstellen des Algorithmus geführt hat), kann verwendet werden, um zu testen, ob der Algorithmus wie gewünscht funktioniert. Kurz gesagt, alles andere als der Code / selbst erstellte Algorithmus kann verwendet werden.
Flater
2
@PaintingInAir: Es ist gefährlich, Kunden, Analysten und Entwickler eng miteinander zu verbinden. Wenn Sie dazu neigen, wichtige Schritte wie das Definieren des Problembeginns zu überspringen . Ich glaube, das machen Sie hier. Sie möchten anscheinend die Korrektheit des Algorithmus testen , anstatt zu prüfen , ob er korrekt implementiert wurde. Aber so macht man das nicht. Das Testen der Implementierung kann mithilfe von Komponententests erfolgen. Um den Algorithmus selbst zu testen , müssen Sie Ihre (getestete) Anwendung verwenden und die Ergebnisse auf Fakten überprüfen. Dieser tatsächliche Test liegt außerhalb des Bereichs Ihrer Codebasis (wie er sein sollte ).
Flater
4
Diese Antwort ist bereits enorm. Es wird dringend empfohlen, einen Weg zu finden, um den ursprünglichen Inhalt neu zu formulieren, sodass Sie ihn einfach in die neue Antwort integrieren können, wenn Sie ihn nicht wegwerfen möchten.
jpmc26
7
Ich bin auch nicht einverstanden mit Ihrer Prämisse. Tests können und sollten unbedingt aufzeigen, wann der Code eine falsche Ausgabe gemäß der Spezifikation erzeugt. Es ist gültig für Tests, um die Ausgaben für einige bekannte Testfälle zu validieren. Außerdem sollte der Koch besser wissen, als "Handseife" als gültige Burgerzutat zu akzeptieren, und der Arbeitgeber hat den Koch mit ziemlicher Sicherheit darüber aufgeklärt, welche Zutaten verfügbar sind.
jpmc26
9

Eigenschaftsprüfung

Manchmal werden mathematische Funktionen durch "Eigenschaftstests" besser erfüllt als durch herkömmliche beispielbasierte Komponententests. Stellen Sie sich zum Beispiel vor, Sie schreiben Unit-Tests für eine ganzzahlige "Multiplikations" -Funktion. Obwohl die Funktion selbst sehr einfach zu sein scheint, wie können Sie sie gründlich testen, wenn dies die einzige Möglichkeit zur Multiplikation ist, ohne dass die Logik in der Funktion selbst vorhanden ist? Sie können Riesen-Tabellen mit erwarteten Ein- / Ausgängen verwenden, dies ist jedoch begrenzt und fehleranfällig.

In diesen Fällen können Sie bekannte Eigenschaften der Funktion testen, anstatt nach bestimmten erwarteten Ergebnissen zu suchen. Bei der Multiplikation wissen Sie möglicherweise, dass das Multiplizieren einer negativen und einer positiven Zahl zu einer negativen Zahl und das Multiplizieren von zwei negativen Zahlen zu einer positiven Zahl usw. führen sollte. Verwenden Sie zufällige Werte und überprüfen Sie, ob diese Eigenschaften für alle erhalten bleiben Testwerte sind eine gute Möglichkeit, solche Funktionen zu testen. Sie müssen im Allgemeinen auf mehr als eine Eigenschaft testen, können jedoch häufig eine endliche Menge von Eigenschaften identifizieren, die zusammen das korrekte Verhalten einer Funktion validieren, ohne das erwartete Ergebnis für jeden Fall zu kennen.

Eine der besten Einführungen in Property Testing, die ich gesehen habe, ist diese in F #. Hoffentlich ist die Syntax kein Hindernis für das Verständnis der Erklärung der Technik.

Aaron M. Eshbach
quelle
1
Ich würde vorschlagen, in Ihrem Beispiel für die Multiplikation etwas Spezifischeres hinzuzufügen, z. B. die Erzeugung von zufälligen Quartetten (a, b, c) und die Bestätigung, dass (ab) (cd) (ac-ad) - (bc-bd) ergibt. Eine Multiplikationsoperation könnte ziemlich fehlerhaft sein und die Regel (negativ mal negativ ergibt positiv) beibehalten, aber die Verteilungsregel sagt bestimmte Ergebnisse voraus.
Supercat
4

Es ist verlockend, den Code zu schreiben und dann zu prüfen, ob das Ergebnis "richtig" aussieht, aber, wie Sie richtig verstehen, ist es keine gute Idee.

Wenn der Algorithmus schwierig ist, können Sie einige Dinge tun, um die manuelle Berechnung des Ergebnisses zu vereinfachen.

  1. Verwenden Sie Excel. Richten Sie eine Tabelle ein, die einen Teil oder die gesamte Berechnung für Sie ausführt. Halten Sie es einfach genug, damit Sie die Schritte sehen können.

  2. Teilen Sie Ihre Methode in kleinere testbare Methoden mit jeweils eigenen Tests auf. Wenn Sie sicher sind, dass die kleineren Teile funktionieren, verwenden Sie sie, um den nächsten Schritt manuell durchzuführen.

  3. Verwenden Sie Aggregateigenschaften zur Überprüfung der Integrität. Angenommen, Sie haben einen Wahrscheinlichkeitsrechner. Sie wissen vielleicht nicht, wie die einzelnen Ergebnisse aussehen sollen, aber Sie wissen, dass sich alle Ergebnisse zu 100% summieren müssen.

  4. Rohe Gewalt. Schreiben Sie ein Programm, das alle möglichen Ergebnisse generiert, und überprüfen Sie, ob keines besser ist als das, was Ihr Algorithmus generiert.

Ewan
quelle
Berücksichtigen Sie bei 3. einige Rundungsfehler. Es ist möglich, dass Ihr Gesamtbetrag 100.000001% oder ähnlich nahe beieinander liegende, aber nicht genaue Zahlen beträgt.
Flater
2
Bei 4 bin ich mir nicht ganz sicher. Wenn Sie in der Lage sind, das optimale Ergebnis für alle möglichen Eingabekombinationen zu generieren (die Sie dann zum Testen der Bestätigung verwenden), sind Sie von Natur aus bereits in der Lage, das optimale Ergebnis zu berechnen, und können daher nicht ' Ich brauche diesen zweiten Teil des Codes, den du zu testen versuchst. Zu diesem Zeitpunkt ist es besser, den vorhandenen Generator für optimale Ergebnisse zu verwenden, da er sich bereits als funktionsfähig erwiesen hat. (und wenn es noch nicht funktioniert, können Sie sich nicht darauf verlassen, dass es funktioniert, um zunächst Ihre Tests zu überprüfen).
Flater
6
@flater in der Regel haben Sie andere Anforderungen sowie Korrektheit, die Brute Force nicht erfüllt. zB Leistung.
Ewan
1
@flater Ich würde es hassen, deine Sorte, deinen kürzesten Weg, deine Schachmaschine usw. zu benutzen, wenn du das glaubst. Aber ich würde den ganzen Tag bei Ihrem Rundungsfehler spielen
Ewan
3
@flater trittst du zurück, wenn du zu einem Königsbauern-Endspiel gelangst? Nur weil das gesamte Spiel nicht brutal gezwungen werden kann, bedeutet dies nicht, dass eine einzelne Position nicht erreicht werden kann. Nur weil Sie den richtigen kürzesten Weg zu einem Netzwerk erzwingen, heißt das nicht, dass Sie den kürzesten Weg in allen Netzwerken kennen
Ewan
2

TL; DR

Lesen Sie den Abschnitt "Vergleichstests", um Ratschläge zu erhalten, die in anderen Antworten nicht enthalten sind.


Anfänge

Beginnen Sie mit dem Testen der Fälle, die vom Algorithmus zurückgewiesen werden sollen ( workPerDayz. B. null oder negativ ), und der Fälle, die trivial sind (z tasks. B. leeres Array).

Danach möchten Sie zuerst die einfachsten Fälle testen. Für die tasksEingabe müssen wir verschiedene Längen testen; es sollte ausreichen, 0, 1 und 2 Elemente zu testen (2 gehört zu der Kategorie "viele" für diesen Test).

Wenn Sie Eingaben finden, die mental berechnet werden können, ist dies ein guter Anfang. Eine Technik, die ich manchmal verwende, ist, von einem gewünschten Ergebnis auszugehen und (in der Spezifikation) zu Eingaben zurückzuarbeiten, die dieses Ergebnis erzeugen sollten.

Vergleichstests

Manchmal ist die Beziehung der Ausgabe zur Eingabe nicht offensichtlich, aber Sie haben eine vorhersehbare Beziehung zwischen verschiedenen Ausgaben, wenn eine Eingabe geändert wird. Wenn ich das Beispiel richtig verstanden habe, erhöht das Hinzufügen einer Aufgabe (ohne andere Eingaben zu ändern) niemals den Anteil der pünktlichen Arbeit, sodass wir einen Test erstellen können, der die Funktion zweimal aufruft - einmal mit und einmal ohne die zusätzliche Aufgabe - und behauptet die Ungleichung zwischen den beiden Ergebnissen.

Fallbacks

Manchmal musste ich auf einen langen Kommentar zurückgreifen, der ein von Hand berechnetes Ergebnis in Schritten anzeigt, die der Spezifikation entsprechen (ein solcher Kommentar ist normalerweise länger als der Testfall). Der schlimmste Fall ist, wenn Sie die Kompatibilität mit einer früheren Implementierung in einer anderen Sprache oder für eine andere Umgebung aufrechterhalten müssen. Manchmal muss man die Testdaten nur mit so etwas wie beschriften /* derived from v2.6 implementation on ARM system */. Das ist nicht sehr befriedigend, kann aber als Wiedergabetest beim Portieren oder als kurzfristige Krücke akzeptabel sein.

Erinnerungen

Das wichtigste Merkmal eines Tests ist seine Lesbarkeit. Wenn die Ein- und Ausgänge für den Leser undurchsichtig sind, hat der Test einen sehr geringen Wert. Wenn dem Leser jedoch geholfen wird, die Beziehungen zwischen ihnen zu verstehen, dient der Test zwei Zwecken.

Vergessen Sie nicht, für ungenaue Ergebnisse (z. B. Gleitkommazahlen) ein geeignetes "ungefähr gleich" zu verwenden.

Vermeiden Sie übermäßiges Testen - fügen Sie einen Test nur hinzu, wenn er etwas abdeckt (z. B. einen Grenzwert), den andere Tests nicht erreichen.

Toby Speight
quelle
2

Diese schwer zu testende Funktion hat nichts Besonderes. Das Gleiche gilt für Code, der externe Schnittstellen verwendet (z. B. eine REST-API einer Drittanbieteranwendung, die nicht von Ihnen gesteuert wird und mit Sicherheit nicht von Ihrer Testsuite getestet werden kann), oder für eine Drittanbieterbibliothek, bei der Sie sich nicht sicher sind, ob dies der Fall ist genaues Byte-Format der Rückgabewerte).

Es ist durchaus sinnvoll, den Algorithmus einfach für eine vernünftige Eingabe auszuführen, zu überprüfen, was er bewirkt, sicherzustellen, dass das Ergebnis korrekt ist, und die Eingabe und das Ergebnis als Testfall zu kapseln. Sie können dies für einige Fälle tun und erhalten so mehrere Proben. Versuchen Sie, die Eingabeparameter so unterschiedlich wie möglich zu gestalten. Im Falle eines externen API-Aufrufs führen Sie einige Aufrufe für das reale System durch, verfolgen sie mit einem Tool und verspotten sie dann in Ihren Komponententests, um zu sehen, wie Ihr Programm reagiert Der Taskplanungscode wird ausgeführt, manuell überprüft und das Ergebnis in den Tests festgeschrieben.

Dann bringen Sie offensichtlich Randfälle wie (in Ihrem Beispiel) eine leere Liste von Aufgaben mit. Sachen wie diese.

Ihre Testsuite ist möglicherweise nicht so gut wie eine Methode, mit der Sie Ergebnisse leicht vorhersagen können. aber immer noch 100% besser als keine Testsuite (oder nur ein Rauchtest).

Wenn Ihr Problem ist allerdings, dass Sie finden es schwer zu entscheiden , ob ein Ergebnis ist korrekt, dann ist das ein ganz anderes Problem. Angenommen, Sie haben eine Methode, die erkennt, ob eine willkürlich große Zahl eine Primzahl ist. Sie können kaum eine zufällige Zahl darauf werfen und dann einfach "schauen", ob das Ergebnis korrekt ist (vorausgesetzt, Sie können die Primzahl in Ihrem Kopf oder auf einem Blatt Papier nicht bestimmen). In diesem Fall gibt es in der Tat wenig, was Sie tun können - Sie müssen entweder bekannte Ergebnisse erhalten (dh einige große Primzahlen) oder die Funktionalität mit einem anderen Algorithmus implementieren (vielleicht sogar einem anderen Team - die NASA scheint das zu mögen dass) und hoffe, dass, wenn eine der beiden Implementierungen fehlerhaft ist, zumindest der Fehler nicht zu den gleichen falschen Ergebnissen führt.

Wenn dies ein normaler Fall für Sie ist, müssen Sie mit Ihren Anforderungsingenieuren ein gutes Gespräch führen. Wenn sie Ihre Anforderungen nicht auf eine Weise formulieren können, die für Sie einfach (oder überhaupt möglich) zu überprüfen ist, wann wissen Sie dann, ob Sie fertig sind?

AnoE
quelle
2

Andere Antworten sind gut, deshalb werde ich versuchen, auf einige Punkte einzugehen, die sie bisher gemeinsam übersehen haben.

Ich habe Software für die Bildverarbeitung mit Synthetic Aperture Radar (SAR) geschrieben (und gründlich getestet). Es ist naturwissenschaftlicher / numerischer Natur (es gibt eine Menge Geometrie, Physik und Mathematik).

Einige Tipps (für allgemeine wissenschaftliche / numerische Tests):

1) Verwenden Sie Inverse. Was ist das fftvon [1,2,3,4,5]? Keine Ahnung. Was ist ifft(fft([1,2,3,4,5]))? Sollte sein [1,2,3,4,5](oder in der Nähe davon, Gleitkommafehler könnten auftauchen). Gleiches gilt für den 2D-Fall.

2) Verwenden Sie bekannte Zusicherungen. Wenn Sie eine Determinantenfunktion schreiben, kann es schwierig sein, die Determinante einer zufälligen 100x100-Matrix zu bestimmen. Aber Sie wissen, dass die Determinante der Identitätsmatrix 1 ist, auch wenn sie 100x100 ist. Sie wissen auch, dass die Funktion 0 in einer nicht invertierbaren Matrix zurückgeben sollte (wie eine 100x100, die mit allen 0en gefüllt ist).

3) Verwenden Sie grobe Aussagen anstelle exakter Aussagen. Ich habe einen Code für die SAR-Verarbeitung geschrieben, mit dem zwei Bilder registriert werden, indem Verbindungspunkte generiert werden, die eine Zuordnung zwischen den Bildern erstellen, und anschließend eine Verzerrung zwischen ihnen vorgenommen wird, um eine Übereinstimmung zu erzielen. Es könnte sich auf einer Subpixel-Ebene registrieren. A priori ist es schwer zu sagen , wie die Registrierung von zwei Bildern aussehen könnte. Wie kannst du es testen? Dinge wie:

EXPECT_TRUE(register(img1, img2).size() < min(img1.size(), img2.size()))

Da Sie sich nur für überlappende Teile registrieren können, muss das registrierte Bild kleiner oder gleich Ihrem kleinsten Bild sein und außerdem:

scale = 255
EXPECT_PIXEL_EQ_WITH_TOLERANCE(reg(img, img), img, .05*scale)

Da ein für sich selbst registriertes Bild für sich selbst nahe sein sollte, aufgrund des vorliegenden Algorithmus jedoch möglicherweise mehr als nur Gleitkommafehler auftreten, prüfen Sie einfach, ob sich jedes Pixel innerhalb von +/- 5% des Bereichs befindet, den die Pixel einnehmen können (0-255 ist Graustufen, wie es in der Bildverarbeitung üblich ist). Das Ergebnis sollte mindestens die gleiche Größe wie die Eingabe haben.

Sie können sogar einfach einen Rauchtest durchführen (dh es anrufen und sicherstellen, dass es nicht abstürzt). Im Allgemeinen ist diese Technik besser für größere Tests, bei denen das Endergebnis nicht (leicht) vor dem Ausführen des Tests berechnet werden kann.

4) Verwenden Sie OR STORE einen Zufallszahlen-Startwert für Ihren RNG.

Läuft sie müssen reproduzierbar sein. Es ist jedoch falsch, dass die einzige Möglichkeit, einen reproduzierbaren Lauf zu erhalten, darin besteht, einem Zufallszahlengenerator einen bestimmten Startwert bereitzustellen. Manchmal sind Zufallstests wertvoll. Ich habe Bugs in wissenschaftlichem Code gesehen / gehört, die in entarteten Fällen auftreten, die zufällig generiert wurden (in komplizierten Algorithmen kann es schwierig sein, den entarteten Fall überhaupt zu erkennen)). Anstatt Ihre Funktion immer mit demselben Startwert aufzurufen, generieren Sie einen zufälligen Startwert, verwenden Sie diesen Startwert und protokollieren Sie den Wert des Startwerts. Auf diese Weise hat jeder Lauf einen anderen zufälligen Startwert. Wenn jedoch ein Absturz auftritt, können Sie das Ergebnis erneut ausführen, indem Sie den Startwert verwenden, den Sie zum Debuggen angemeldet haben. Ich habe dies tatsächlich in der Praxis verwendet und es hat einen Fehler beseitigt, also dachte ich mir, ich würde es erwähnen. Zugegeben, das ist nur einmal passiert, und ich bin mir sicher, dass es sich nicht immer lohnt, diese Technik mit Vorsicht anzuwenden. Zufall mit dem gleichen Samen ist jedoch immer sicher. Nachteil (im Gegensatz dazu, dass immer nur derselbe Startwert verwendet wird): Sie müssen Ihre Testläufe protokollieren. Oberseite: Korrektheit und Bugnuking.

Ihr besonderer Fall

1) Prüfen Sie, ob ein leerer taskArray Wert 0 zurückgibt (bekannte Zusicherung).

2) Erzeugen Sie gelegentlichen Eingang , so dass task.time > 0 , task.due > 0, und task.importance > 0 für all task s, und behauptet , das Ergebnis größer als 0 (Grob assert, zufällige Input) . Sie müssen nicht verrückt werden und zufällige Samen erzeugen, Ihr Algorithmus ist einfach nicht komplex genug, um dies zu rechtfertigen. Es gibt ungefähr keine Chance, dass es sich auszahlt: Halten Sie den Test einfach.

3) Teste ob task.importance == 0 für alle task s, dann ist das Ergebnis 0 (bekannte Behauptung)

4) Andere Antworten haben dies angesprochen, aber es kann für Ihren speziellen Fall wichtig sein : Wenn Sie eine API erstellen, die von Benutzern außerhalb Ihres Teams verwendet werden soll, müssen Sie die entarteten Fälle testen. workPerDay == 0Stellen Sie beispielsweise sicher, dass Sie einen schönen Fehler ausgeben, der dem Benutzer mitteilt, dass die Eingabe ungültig ist. Wenn Sie keine API erstellen und diese nur für Sie und Ihr Team ist, können Sie diesen Schritt wahrscheinlich überspringen und es einfach ablehnen, sie mit dem entarteten Fall aufzurufen.

HTH.

Matt Messersmith
quelle
1

Integrieren Sie Assertion-Tests in Ihre Unit-Test-Suite, um Ihren Algorithmus eigenschaftsbasiert zu testen. Zusätzlich zum Schreiben von Komponententests, die auf bestimmte Ausgaben prüfen, können Sie auch Tests schreiben, die fehlschlagen, indem sie Assertionsfehler im Hauptcode auslösen.

Viele Algorithmen stützen sich für ihre Korrektheitsnachweise auf die Aufrechterhaltung bestimmter Eigenschaften während der Stufen des Algorithmus. Wenn Sie diese Eigenschaften sinnvoll überprüfen können, indem Sie sich die Ausgabe einer Funktion ansehen, reicht ein Komponententest aus, um Ihre Eigenschaften zu testen. Andernfalls können Sie beim Assertion-basierten Testen jedes Mal testen, ob eine Implementierung eine Eigenschaft beibehält, wenn der Algorithmus dies annimmt.

Assertion-based Testing wird Algorithmusfehler, Codierungsfehler und Implementierungsfehler aufgrund von Problemen wie numerischer Instabilität aufdecken. In vielen Sprachen werden Assertions beim Kompilieren oder vor der Interpretation des Codes entfernt, damit die Assertions im Produktionsmodus nicht zu Leistungseinbußen führen. Wenn Ihr Code Unit-Tests besteht, in einem realen Fall jedoch fehlschlägt, können Sie die Zusicherungen als Debugging-Tool wieder aktivieren.

Tobias Hagge
quelle
1

Einige der anderen Antworten hier sind sehr gut:

  • Testen Sie Basis-, Kanten- und Eckgehäuse
  • Führen Sie Hygienekontrollen durch
  • Vergleichstests durchführen

... ich würde noch ein paar andere Taktiken hinzufügen:

  • Zerlegen Sie das Problem.
  • Beweisen Sie den Algorithmus außerhalb des Codes.
  • Testen Sie, ob der [extern bewährte] Algorithmus wie geplant implementiert ist.

Durch die Zerlegung können Sie sicherstellen, dass die Komponenten Ihres Algorithmus genau das tun, was Sie von ihnen erwarten. Und mit einer "guten" Zersetzung können Sie auch sicherstellen, dass sie richtig zusammengeklebt sind. Eine gute Zerlegung verallgemeinert und vereinfacht den Algorithmus so weit, dass Sie die Ergebnisse (der vereinfachten, generischen Algorithmen) von Hand gut genug vorhersagen können , um gründliche Tests zu schreiben.

Wenn Sie nicht in diesem Ausmaß zerlegen können, beweisen Sie den Algorithmus außerhalb des Codes mit allen Mitteln, die ausreichen, um Sie und Ihre Kollegen, Stakeholder und Kunden zufrieden zu stellen. Zerlegen Sie dann nur so viel, dass Sie nachweisen können, dass Ihre Implementierung mit dem Design übereinstimmt.

Svidgen
quelle
0

Dies mag wie eine idealistische Antwort erscheinen, hilft jedoch dabei, verschiedene Arten von Tests zu identifizieren.

Wenn strenge Antworten für die Implementierung wichtig sind, sollten Beispiele und erwartete Antworten in den Anforderungen angegeben werden, die den Algorithmus beschreiben. Diese Anforderungen sollten einer Gruppenüberprüfung unterzogen werden. Wenn Sie nicht dieselben Ergebnisse erhalten, muss der Grund ermittelt werden.

Auch wenn Sie sowohl als Analyst als auch als Implementierer auftreten, sollten Sie Anforderungen erstellen und überprüfen lassen, lange bevor Sie Komponententests schreiben. In diesem Fall kennen Sie die erwarteten Ergebnisse und können Ihre Tests entsprechend schreiben.

Wenn dies jedoch ein Teil ist, den Sie implementieren, der entweder nicht Teil der Geschäftslogik ist oder der eine Geschäftslogikantwort unterstützt, sollte es in Ordnung sein, den Test auszuführen, um die Ergebnisse zu ermitteln und dann den zu erwartenden Test zu ändern diese Ergebnisse. Die Endergebnisse werden bereits mit Ihren Anforderungen verglichen. Wenn sie also korrekt sind, muss der gesamte Code, der diese Endergebnisse liefert, numerisch korrekt sein. An diesem Punkt dienen Ihre Komponententests eher zum Erkennen von Randversagensfällen und künftigen Umgestaltungsänderungen als zum Nachweis, dass dies gegeben ist Algorithmus erzeugt korrekte Ergebnisse.

Bill K
quelle
0

Ich denke, es ist durchaus akzeptabel, den Prozess gelegentlich zu verfolgen:

  • Entwerfen Sie einen Testfall
  • Verwenden Sie Ihre Software, um die Antwort zu erhalten
  • Überprüfen Sie die Antwort von Hand
  • Schreiben Sie einen Regressionstest, damit zukünftige Versionen der Software diese Antwort weiterhin geben.

Dies ist ein vernünftiger Ansatz in jeder Situation, in der es einfacher ist, die Richtigkeit einer Antwort von Hand zu überprüfen, als die Antwort von Hand nach den ersten Prinzipien zu berechnen.

Ich kenne Leute, die Software zum Rendern von gedruckten Seiten schreiben und Tests durchführen, die überprüfen, ob auf der gedruckten Seite genau die richtigen Pixel eingestellt sind. Der einzig vernünftige Weg, dies zu tun, besteht darin, den Code zum Rendern der Seite zu schreiben, mit den Augen zu überprüfen, ob sie gut aussieht, und das Ergebnis dann als Regressionstest für zukünftige Versionen zu erfassen.

Nur weil Sie in einem Buch gelesen haben, dass eine bestimmte Methodik dazu anregt, zuerst die Testfälle zu schreiben, müssen Sie dies nicht immer so tun. Regeln sind da, um gebrochen zu werden.

Michael Kay
quelle
0

Bei anderen Antworten gibt es bereits Techniken, wie ein Test aussieht, wenn das spezifische Ergebnis nicht außerhalb der getesteten Funktion ermittelt werden kann.

Was ich zusätzlich tue, was ich in den anderen Antworten nicht gesehen habe, ist, Tests auf irgendeine Weise automatisch zu generieren:

  1. Zufällige Eingaben
  2. Iteration über Datenbereiche
  3. Konstruktion von Testfällen aus Grenzsätzen
  4. Alles oben.

Wenn die Funktion beispielsweise drei Parameter mit jeweils einem zulässigen Eingabebereich [-1,1] akzeptiert, testen Sie alle Kombinationen der einzelnen Parameter {-2, -1,01, -1, -0,99, -0,5, -0,01, 0,0,01 0,5, 0, 0, 99, 1, 01, 2, etwas mehr zufällig in (-1, 1)}

Kurz gesagt: Manchmal kann schlechte Qualität durch Quantität subventioniert werden.

Keith
quelle