Ich arbeite gerade an einer Art Compiler, der mit sablecc erstellt wurde.
Um es kurz zu machen, der Compiler nimmt sowohl die Spezifikationsdateien (das ist was wir analysieren) als auch die .class-Dateien als Input und wird den .class-Datei-Bytecode instrumentieren, um sicherzustellen, dass beim Ausführen der .class-Dateien alle der Spezifikationen wird nicht verletzt (das ist ein bisschen wie jml / Code Verträge! aber viel mächtiger).
Wir haben einige Dutzend Systemtests, die einen großen Teil der Analysephase abdecken (im Zusammenhang damit, sicherzustellen, dass die Spezifikationen sinnvoll sind und dass sie auch in Übereinstimmung mit den .class-Dateien stehen, die sie spezifizieren sollen).
Wir haben sie in zwei Gruppen unterteilt: die gültigen Tests und die ungültigen Tests.
Die gültigen Tests bestehen aus Quellcodedateien, die bei Kompilierung durch unseren Compiler keine Compilerfehler / Warnungen anzeigen sollten.
Die ungültigen Tests bestehen aus Quellcodedateien, die bei Kompilierung durch unseren Compiler mindestens einen Compilerfehler / Warnung anzeigen sollten.
Dies hat uns in der Analysephase gute Dienste geleistet. Die Frage ist jetzt, wie man die Codegenerierungsphase testet. Ich habe in der Vergangenheit Systemtests über einen kleinen Compiler gemacht, den ich in einem Compilerkurs entwickelt habe. Jeder Test würde aus ein paar Quelldateien dieser Sprache und einem output.txt
bestehen. Wenn ich den Test ausführte, würde ich die Quelldateien kompilieren und dann die Hauptmethode ausführen und überprüfen, ob das Ausgabeergebnis gleich output.txt
wäre. All das war natürlich automatisiert.
Nun, mit diesem größeren Compiler / Bytecode-Instrumentator sind die Dinge nicht so einfach. Es ist keine einfache Aufgabe, das nachzubilden, was ich mit meinem einfachen Compiler gemacht habe. Ich denke, der beste Weg ist, sich in diesem Stadium von den Systemtests zurückzuziehen und sich auf Komponententests zu konzentrieren.
Wie jeder Compiler-Entwickler weiß, besteht ein Compiler aus vielen Besuchern. Ich bin mir nicht sicher, wie ich mit dem Unit-Testing fortfahren soll. Nach dem, was ich gesehen habe, rufen die meisten Besucher eine Gegenklasse an, die Methoden für diesen Besucher hat (ich denke, die Idee war, die SRP für die Besucher zu behalten).
Es gibt ein paar Techniken, die ich anwenden kann, um meinen Compiler zu testen:
Unit testet jede einzelne Methode des Besuchers separat. Dies scheint eine gute Idee für einen stackless Besucher zu sein, sieht aber für Besucher, die einen (oder mehrere) Stacks verwenden, eine schreckliche Idee aus. Ich gehe dann auch daran, jede der anderen Methoden aus der Standardklasse (Lesen, Nicht-Besucher) auf traditionelle Weise zu testen.
Unit testet den gesamten Besucher auf einmal. Das heißt, ich erstelle einen Baum, den ich dann besuche. Am Ende überprüfe ich, ob die Symboltabelle korrekt aktualisiert wurde oder nicht. Es macht mir nichts aus, über seine Abhängigkeiten zu spotten.
Das gleiche wie 2), aber jetzt verspotten die Abhängigkeiten des Besuchers.
Was andere?
Ich habe immer noch das Problem, dass die Komponententests sehr eng mit dem AST von sabbleCC gekoppelt sind (was tbh wirklich hässlich ist).
Wir machen derzeit keine neuen Tests, aber ich würde den Zug gerne wieder in die Spur bringen, da ich sicher bin, dass das Testen des Systems nicht dasselbe ist wie das Füttern eines Monsters, das früher oder später wieder zum Biss kommen wird uns in den Hintern, wenn wir es am wenigsten erwarten - (
Hat jemand Erfahrungen mit Compiler-Tests gemacht, die einige hilfreiche Ratschläge geben könnten, wie man jetzt vorgeht? Ich bin hier irgendwie verloren!
Ich bin an einem Projekt beteiligt, bei dem ein Java AST mit dem Eclipse-Compiler in eine andere Sprache, OpenCL, übersetzt wird und ähnliche Probleme aufweist.
Ich habe keine magischen Lösungen für Sie, aber ich werde meine Erfahrung teilen, falls es hilft.
Ihre Technik des Testens mit der erwarteten Ausgabe (mit output.txt) ist, wie ich auch begann, aber es wurde ein absoluter Wartungsalarm für die Tests. Als ich aus irgendeinem Grund den Generator oder die Ausgabe ändern musste (was einige Male passierte), musste ich alle erwarteten Ausgabedateien neu schreiben - und es gab riesige Mengen von ihnen. Ich fing an, die Ausgabe überhaupt nicht ändern zu wollen, aus Angst, alle Tests zu brechen (was schlecht war), aber am Ende habe ich sie verschrottet und stattdessen den daraus resultierenden AST getestet. Dies bedeutete, dass ich die Ausgabe "locker" testen konnte. Zum Beispiel, wenn ich die Erzeugung von if-Anweisungen testen wollte, konnte ich nur die einzige if-Anweisung in der generierten Klasse finden (ich schrieb Hilfsmethoden, um all diese üblichen AST-Sachen zu machen), verifiziere ein paar Dinge darüber und getan werden. Dieser Test interessiert nicht, wie die Klasse benannt wurde oder ob es zusätzliche Anmerkungen oder Kommentare gab. Dies funktionierte ziemlich gut, da die Tests konzentrierter waren. Der Nachteil ist, dass die Tests enger an den Code gekoppelt waren. Wenn ich also jemals die Eclipse-Compiler / AST-Bibliothek ausreißen und etwas anderes verwenden wollte, musste ich alle meine Tests neu schreiben. Am Ende, weil sich die Codegenerierung im Laufe der Zeit ändern würde, war ich bereit, diesen Preis zu bezahlen.
Ich bin auch stark auf Integrationstests angewiesen - Tests, die den generierten Code in der Zielsprache kompilieren und ausführen. Ich hatte viel mehr dieser Arten von Tests als Unit Tests, nur weil sie nützlicher zu sein schienen und mehr Probleme auffingen.
Was Besucher-Tests betrifft, teste ich wieder mehr Integrationstests mit ihnen - besorge dir eine wirklich kleine / spezifische Java-Quelldatei, lade sie mit dem Eclipse-Compiler, führe einen meiner Besucher damit aus und überprüfe die Ergebnisse. Die einzige andere Möglichkeit zu testen, ohne den Eclipse-Compiler aufzurufen wäre, einen kompletten AST zu simulieren, was einfach nicht durchführbar war - die meisten Besucher waren nicht-trivial und benötigten einen vollständig aufgebauten / gültigen Java-AST, da sie Annotationen aus der Hauptklasse lesen würden . Die meisten Besucher waren auf diese Weise testbar, weil sie entweder kleine OpenCL-Codefragmente generiert oder eine Datenstruktur aufgebaut hatten, die die Unit-Tests verifizieren konnten.
Ja, alle meine Tests sind sehr eng an den Eclipse-Compiler gekoppelt. Aber das ist auch die Software, die wir schreiben. Wenn wir etwas anderes verwenden würden, müssten wir sowieso das ganze Programm umschreiben, also ist es ein Preis, den wir gerne bezahlen. Ich denke, es gibt keine einzige Lösung - Sie müssen die Kosten für die enge Kopplung gegenüber der Testwartbarkeit / Einfachheit abwägen.
Wir haben auch eine Menge Test-Utility-Code, wie zum Beispiel das Einrichten des Eclipse-Compilers mit Standardeinstellungen, Code zum Herausziehen der Body-Knoten von Methodenbäumen usw. Wir versuchen, die Tests so klein wie möglich zu halten (I Ich weiß, das ist wahrscheinlich der gesunde Menschenverstand, aber möglicherweise erwähnenswert).
(Bearbeitungen / Ergänzungen in Antworten auf Kommentare - einfacher zu lesen / zu formatieren als Kommentarantworten)
"Ich verlasse mich auch stark auf Integrationstests - Tests, die den generierten Code in der Zielsprache tatsächlich kompilieren und ausführen" Was haben diese Tests eigentlich gemacht? Wie unterscheiden sie sich von den output.txt-Tests?
(Nochmal bearbeiten: Nachdem ich die Frage erneut gelesen habe, merke ich, dass unsere Ansätze gleich sind, ignoriere das also)
Anstatt nur Quellcode zu generieren und diesen mit der erwarteten Ausgabe zu vergleichen, die ich anfangs gemacht habe, generieren die Integrationstests OpenCL-Code, kompilieren ihn und führen ihn aus. Der gesamte erzeugte Code erzeugt eine Ausgabe, und diese Ausgabe wird dann verglichen.
Ich habe zum Beispiel eine Java-Klasse, die, wenn der Generator richtig funktioniert, OpenCL-Code erzeugen soll, der Werte in zwei Puffern summiert und den Wert in einen dritten Puffer legt. Anfangs hätte ich eine Textdatei mit dem erwarteten OpenCL-Code geschrieben und diese in meinem Test verglichen. Jetzt erzeugt der Integrationstest den Code, führt ihn durch den OpenCL-Compiler, führt ihn aus und der Test überprüft dann die Werte.
"Was Besucher-Tests betrifft, teste ich wieder mehr Integrationstests mit ihnen - erhalte eine wirklich kleine / spezifische Java-Quelldatei, lade sie mit dem Eclipse-Compiler, führe einen meiner Besucher damit aus und überprüfe die Ergebnisse." Du meinst mit einem deiner Besucher rennen oder alle Besucher zu dem Besucher laufen lassen, den du testen willst?
Die meisten Besucher konnten unabhängig voneinander geführt werden. Wo es möglich war, würde ich nur mit dem Besucher rennen, den ich gerade teste, oder wenn es eine Abhängigkeit von anderen gibt, würde die minimale Anzahl von Besuchern benötigt (normalerweise war nur eine andere erforderlich). Die Besucher sprechen nicht direkt miteinander, sondern benutzen Kontextobjekte, die herumgereicht werden.Diese können künstlich in den Tests konstruiert werden, um Dinge in einen bekannten Zustand zu versetzen.
Andere Frage, verwenden Sie Mocks - überhaupt, in diesem Projekt? Benutzen Sie regelmäßig Mocks in anderen Projekten? Ich versuche nur, ein klares Bild von der Person zu bekommen, mit der ich rede: P
In diesem Projekt verwenden wir Mocks in etwa 5% der Tests, wahrscheinlich sogar weniger. Und ich verspotte keinen Eclipse-Compiler-Kram.
Die Sache mit Mocks ist, dass ich verstehen müsste, was ich gut ausspreche, und das ist beim Eclipse-Compiler nicht der Fall. Es gibt eine ganze Reihe von Besuchermethoden, die aufgerufen werden, und manchmal bin ich mir nicht sicher, welche aufgerufen werden soll (zB visit ExtendedStringLiteral oder besuche StringLiteral für Stringliterale?) Wenn ich das ausgeheckt habe und das eine oder andere angenommen habe , dies könnte nicht der Realität entsprechen und das Programm würde fehlschlagen, selbst wenn die Tests bestanden würden - nicht erwünscht. Die einzigen Mocks, die wir machen, sind ein Paar für die Annotation-Prozessor-API, ein paar Eclipse-Compileradapter und einige unserer eigenen Kernklassen.
Andere Projekte, wie Java EE, mehr Mocks wurden verwendet, aber ich bin immer noch kein begeisterter Benutzer von ihnen. Je definierter, verständlicher und vorhersagbarer eine API ist, desto wahrscheinlicher werde ich die Verwendung von Mocks in Erwägung ziehen.
Die ersten Phasen unseres Programms sind wie bei einem normalen Compiler. Wir extrahieren Informationen aus den Quelldateien und füllen eine (große und komplexe!) Symboltabelle aus. Wie würdest du das System testen? Theoretisch könnte ich einen Test mit den Quelldateien erstellen und auch eine symbolTable.txt (oder .xml oder was auch immer), die alle Informationen über die symbolTable enthält, aber das wäre, denke ich, etwas komplex. Jeder dieser Integrationstests wäre eine komplexe Aufgabe!
Ich würde versuchen, kleine Teile der Symboltabelle anstatt der ganzen Menge auf einmal zu testen. Wenn ich testen würde, ob eine Java-Struktur korrekt erstellt wurde, hätte ich etwas wie:
ein Test nur für if-Anweisungen:
mindestens einen Test für jede andere Art von Anweisung in einem ähnlichen Stil.
Bei diesem Ansatz handelt es sich um eine Integrationstests, aber jeder Integrationstest testet nur einen kleinen Teil des Systems.
Im Wesentlichen würde ich versuchen, die Tests so klein wie möglich zu halten. Ein großer Teil des Testcodes zum Herausziehen von Bits des Baums kann in Hilfsmethoden verschoben werden, um die Testklassen klein zu halten.
Ich dachte, dass ich vielleicht einen hübschen Drucker erstellen könnte, der die Symboltabelle übernehmen und die entsprechenden Quelldateien ausgeben würde (wenn alles in Ordnung wäre, würde es genauso aussehen wie die ursprünglichen Quelldateien). Das Problem ist, dass die Originaldateien Dinge in einer anderen Reihenfolge haben können als das, was mein hübscher Drucker druckt. Ich befürchte, dass ich mit diesem Ansatz nur eine weitere Dose Würmer öffne. Ich habe Teile des Codes unerbittlich umgeformt und die Bugs beginnen sich zu zeigen. Ich brauche wirklich einige Integrationstests, um mich auf dem richtigen Weg zu halten.
Das ist genau der Ansatz, den ich gewählt habe. In meinem System ändert sich die Reihenfolge der Dinge jedoch nicht sehr. Ich habe Generatoren, die im Wesentlichen Code als Reaktion auf Java AST-Knoten ausgeben, aber es gibt ein bisschen Freiheit darin, dass Generatoren sich selbst rekursiv nennen können. Zum Beispiel kann der 'if' Generator, der als Antwort auf einen Java If Statement AST-Knoten abgefeuert wird, 'if (') ausschreiben, dann andere Generatoren bitten, die Bedingung zu rendern, dann '' schreiben '', andere Generatoren zum Schreiben auffordern aus dem Körper, dann schreibe '}'.
Tags und Links java unit-testing testing compiler-design sablecc