Wie teste ich, wenn die Anordnung der Daten zu umständlich ist?

19

Ich schreibe einen Parser und als Teil davon habe ich eine ExpanderKlasse, die eine einzelne komplexe Anweisung in mehrere einfache Anweisungen "erweitert". Zum Beispiel würde es dies erweitern:

x = 2 + 3 * a

in:

tmp1 = 3 * a
x = 2 + tmp1

Jetzt überlege ich, wie ich diese Klasse testen soll, insbesondere, wie ich die Tests anordnen soll. Ich könnte den Eingabesyntaxbaum manuell erstellen:

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

Oder ich könnte es als String schreiben und analysieren:

var input = new Parser().ParseStatement("x = 2 + 3 * a");

Die zweite Option ist viel einfacher, kürzer und lesbar. Es führt aber auch eine Abhängigkeit von ein Parser, was bedeutet, dass ein Fehler in Parserdiesem Test fehlschlagen könnte. Der Test würde also aufhören, ein Komponententest von Expanderund zu sein, und ich denke, er wird technisch zu einem Integrationstest von Parserund Expander.

Meine Frage ist: Ist es in Ordnung, sich zum Testen dieser ExpanderKlasse größtenteils (oder vollständig) auf diese Art von Integrationstests zu verlassen ?

svick
quelle
3
Dass ein Bug-In Parsereinen anderen Test nicht bestehen kann, ist kein Problem, wenn Sie gewohnheitsmäßig nur bei null Fehlern einen Fehler begehen Parser. Ich würde mir eher Sorgen machen, dass ein Fehler Parserdiesen Test zum Erfolg führen könnte, wenn er fehlschlagen sollte . Unit-Tests sind schließlich da, um Fehler zu finden - ein Test ist kaputt, wenn er es nicht tut, aber sollte.
Jonas Kölker

Antworten:

27

Sie werden feststellen, dass Sie viel mehr Tests schreiben, die viel komplizierter, interessanter und nützlicher sind, wenn Sie dies einfach tun können. Also die Option, die beinhaltet

var input = new Parser().ParseStatement("x = 2 + 3 * a");

ist durchaus gültig. Es hängt von einer anderen Komponente ab. Aber alles hängt von Dutzenden anderer Komponenten ab. Wenn Sie etwas auf einen Zentimeter genau verspotten, sind Sie wahrscheinlich auf viele Verspottungsfunktionen und Testgeräte angewiesen.

Entwickler manchmal über Fokus auf der Reinheit ihrer Komponententests oder Tests und Entwicklungseinheit Komponententests nur ohne Modul, Integration, Stress oder andere Arten von Tests. Alle diese Formulare sind gültig und nützlich, und sie liegen in der Verantwortung der Entwickler - nicht nur der Q / A-Mitarbeiter oder des Betriebspersonals weiter unten in der Pipeline.

Ein Ansatz, den ich verwendet habe, besteht darin, mit diesen Läufen auf höherer Ebene zu beginnen und dann die daraus gewonnenen Daten zu verwenden, um den Ausdruck des Tests mit dem niedrigsten gemeinsamen Nenner in Langform zu erstellen. Wenn Sie beispielsweise die Datenstruktur aus der inputoben erstellten sichern, können Sie auf einfache Weise Folgendes erstellen :

var input = new AssignStatement(
    new Variable("x"),
    new BinaryExpression(
        new Constant(2),
        BinaryOperator.Plus,
        new BinaryExpression(new Constant(3), BinaryOperator.Multiply, new Variable("a"))));

Art von Test, der auf der niedrigsten Ebene testet. Auf diese Weise erhalten Sie eine schöne Mischung: Eine Handvoll der grundlegendsten, primitivsten Tests (reine Komponententests), aber Sie haben noch keine Woche damit verbracht, Tests auf dieser primitiven Ebene zu schreiben. Das gibt Ihnen die Zeitressource, die Sie benötigen, um viel mehr, etwas weniger Atomtests mit dem Parserals Helfer zu schreiben . Endergebnis: Mehr Tests, mehr Abdeckung, mehr Eckdaten und andere interessante Fälle, besserer Code und höhere Qualitätssicherung.

Jonathan Eunice
quelle
2
Das ist sinnvoll - vor allem, wenn man bedenkt, dass alles von vielen anderen abhängt. Ein guter Komponententest sollte das minimal mögliche testen. Alles, was innerhalb dieser Mindestmenge liegt, sollte durch einen vorhergehenden Einheitentest geprüft werden. Wenn Sie Parser vollständig getestet haben, können Sie davon ausgehen, dass Sie Parser sicher zum Testen von ParseStatement verwenden können
Jon Story
6
Das Hauptanliegen in Bezug auf die Reinheit (glaube ich) ist es, das Schreiben von zirkulären Abhängigkeiten in Ihren Unit-Tests zu vermeiden. Wenn entweder der Parser- oder der Parser-Test den Expander verwenden und dieser Expander-Test davon abhängt, dass der Parser funktioniert, besteht das schwer zu handhabende Risiko, dass Sie nur testen, dass der Parser und der Expander konsistent sind Sie wollten testen, ob der Expander tatsächlich das tut, was er soll . Solange es jedoch keine umgekehrte Abhängigkeit gibt, unterscheidet sich die Verwendung von Parser in diesem Komponententest nicht wesentlich von der Verwendung einer Standardbibliothek in einem Komponententest.
Steve Jessop
@SteveJessop Guter Punkt. Es ist wichtig, unabhängige Komponenten zu verwenden.
Jonathan Eunice
3
In Fällen, in denen der Parser selbst eine teure Operation ist (z. B. das Lesen von Daten aus Excel-Dateien über com interop), habe ich Testgenerierungsmethoden geschrieben, die den Parser ausführen und den Code auf der Konsole ausgeben, um die vom Parser zurückgegebene Datenstruktur wiederherzustellen . Ich kopiere dann die Ausgabe vom Generator in konventionellere Komponententests. Auf diese Weise wird die Abhängigkeit verringert, da der Parser nur dann ordnungsgemäß funktionieren muss, wenn die Tests nicht bei jeder Ausführung erstellt wurden. (Nicht ein paar Sekunden zu verschwenden / Test zu erstellen / zerstören Excel-Prozesse war ein netter Bonus.)
Dan Neely
+1 für den Ansatz von @ DanNeely. Wir verwenden etwas Ähnliches, um mehrere serialisierte Versionen unseres Datenmodells als Testdaten zu speichern, damit wir sicher sein können, dass neuer Code auch mit älteren Daten funktioniert.
Chris Hayes
6

Natürlich ist es in Ordnung!

Sie benötigen immer einen Funktions- / Integrationstest, der den gesamten Codepfad abdeckt. Ein vollständiger Codepfad bedeutet in diesem Fall, dass der generierte Code ausgewertet wird. Das ist, dass Sie diese Analyse testenx = 2 + 3 * a - Code erzeugt, wenn Lauf mit a = 5Willen Satz xzu 17und wenn Laufe mit a = -2gesetzt xzu -4.

Darunter sollten Sie Komponententests für kleinere Bits durchführen , solange dies tatsächlich zum Debuggen des Codes beiträgt . Je detaillierter die Tests sind, desto höher ist die Wahrscheinlichkeit, dass auch Änderungen am Code vorgenommen werden müssen, da sich die interne Schnittstelle ändert. Solche Tests haben wenig langfristigen Wert und führen zu zusätzlichen Wartungsarbeiten. Es gibt also einen Punkt, an dem die Renditen sinken, und Sie sollten davor aufhören.

Jan Hudec
quelle
4

Unit-Tests ermöglichen es Ihnen, bestimmte Elemente zu lokalisieren, die brechen und wo im Code sie brachen. Sie eignen sich also für sehr feinkörnige Tests. Gute Unit-Tests verkürzen die Debugging-Zeit.

Meiner Erfahrung nach sind Unit-Tests jedoch selten gut genug, um den korrekten Betrieb zu überprüfen. Integrationstests sind daher auch hilfreich, um eine Kette oder Abfolge von Vorgängen zu überprüfen. Integrationstests unterstützen Sie bei der Durchführung von Funktionstests. Wie Sie jedoch betont haben, ist es aufgrund der Komplexität der Integrationstests schwieriger, die spezifische Stelle im Code zu finden, an der der Test unterbrochen wird. Es ist auch etwas spröder, da Fehler an einer beliebigen Stelle in der Kette dazu führen, dass der Test fehlschlägt. Sie werden diese Kette jedoch weiterhin im Produktionscode haben, so dass das Testen der tatsächlichen Kette weiterhin hilfreich ist.

Idealerweise hätten Sie beides, aber in jedem Fall ist ein automatisierter Test im Allgemeinen besser als kein Test.

Peter Smith
quelle
0

Führen Sie viele Tests mit dem Parser durch. Wenn der Parser die Tests besteht, speichern Sie diese Ausgaben in einer Datei, um den Parser zu verspotten und die andere Komponente zu testen.

Tulains Córdova
quelle