Ich bin fest davon überzeugt, dass die Verwendung von Tests, die ein vollständiges Programm verifizieren (z. B. Konvergenztests), einschließlich eines automatisierten Satzes von Regressionstests , von Nutzen ist . Nachdem ich einige Programmierbücher gelesen habe, habe ich das quälende Gefühl, dass ich Unit-Tests schreiben sollte (dh Tests, die die Richtigkeit einer einzelnen Funktion überprüfen und nicht den gesamten Code ausführen, um ein Problem zu lösen) . Allerdings Unit - Tests scheinen nicht immer passen mit wissenschaftlichen Codes und künstlichen oder wie eine Verschwendung von Zeit am Ende des Gefühl.
Sollten wir Komponententests für Forschungscodes schreiben?
programming-paradigms
testing
David Ketcheson
quelle
quelle
Antworten:
Viele Jahre lang hatte ich das Missverständnis, dass ich nicht genug Zeit hatte, Komponententests für meinen Code zu schreiben. Wenn ich Tests schrieb, waren sie aufgebläht, schwere Dinge, die mich nur ermutigten zu glauben, dass ich Komponententests nur schreiben sollte, wenn ich wusste, dass sie gebraucht wurden.
Dann habe ich angefangen, Test Driven Development zu verwenden und fand, dass es eine vollständige Offenbarung ist. Ich bin jetzt fest davon überzeugt, dass ich nicht die Zeit habe, Unit-Tests nicht zu schreiben .
Wenn Sie mit Blick auf das Testen entwickeln, erhalten Sie meiner Erfahrung nach sauberere Schnittstellen, fokussiertere Klassen und Module und im Allgemeinen mehr SOLIDEN , testbaren Code.
Jedes Mal, wenn ich mit Legacy-Code arbeite, der keine Komponententests hat und manuell etwas testen muss, denke ich, "das wäre viel schneller, wenn dieser Code bereits Komponententests hätte". Jedes Mal, wenn ich versuchen muss, dem Code mit hoher Kopplung Unit-Test-Funktionen hinzuzufügen, denke ich, dass dies so viel einfacher wäre, wenn er entkoppelt geschrieben worden wäre.
Vergleich und Gegenüberstellung der beiden von mir unterstützten Versuchsstationen. Einer ist schon eine Weile da und hat viel älteren Code, während der andere relativ neu ist.
Wenn Sie dem alten Labor Funktionen hinzufügen, müssen Sie oft mehrere Stunden damit verbringen, sich mit den Auswirkungen der von Ihnen benötigten Funktionen und der Art und Weise, wie ich diese Funktionen hinzufügen kann, ohne die anderen Funktionen zu beeinträchtigen, auseinanderzusetzen. Der Code ist einfach nicht für Offline-Tests eingerichtet, daher muss so ziemlich alles online entwickelt werden. Wenn ich versuchen würde, offline zu entwickeln, würde ich am Ende mehr Scheinobjekte haben, als vernünftig wären.
Im neueren Labor kann ich normalerweise Funktionen hinzufügen, indem ich sie offline an meinem Schreibtisch entwickle, nur die Dinge verspotte, die sofort benötigt werden, und dann nur eine kurze Zeit im Labor verbringe, um alle verbleibenden Probleme auszubügeln, die nicht behoben wurden -Linie.
Aus Gründen der Übersichtlichkeit und da @ naught101 gefragt ...
Ich arbeite in der Regel an Software für experimentelle Steuerung und Datenerfassung mit Ad-hoc-Datenanalyse. Daher hilft die Kombination von TDD mit Revisionskontrolle, sowohl Änderungen der zugrunde liegenden Experiment-Hardware als auch Änderungen der Datenerfassungsanforderungen im Laufe der Zeit zu dokumentieren.
Sogar in der Situation, in der der Explorationscode entwickelt wurde, konnte ich einen signifikanten Vorteil aus der Kodifizierung von Annahmen und der Fähigkeit erkennen, die Entwicklung dieser Annahmen im Laufe der Zeit zu beobachten.
quelle
Wissenschaftliche Codes haben in der Regel häufiger Konstellationen ineinandergreifender Funktionen als die Geschäftscodes, an denen ich gearbeitet habe, normalerweise aufgrund der mathematischen Struktur des Problems. Daher halte ich Unit-Tests für einzelne Funktionen nicht für sehr effektiv. Ich denke jedoch, dass es eine Klasse von Komponententests gibt, die effektiv sind und sich von ganzen Programmtests insofern stark unterscheiden, als sie auf bestimmte Funktionen abzielen.
Ich definiere nur kurz, was ich unter solchen Tests verstehe. Beim Regressionstest wird nach Änderungen am vorhandenen Verhalten gesucht (die auf irgendeine Weise validiert werden), wenn Änderungen am Code vorgenommen werden. Beim Unit-Test wird ein Teil des Codes ausgeführt und anhand einer Spezifikation überprüft, ob die gewünschte Ausgabe erfolgt. Sie sind nicht so unterschiedlich, da der ursprüngliche Regressionstest ein Komponententest war , da ich feststellen musste, dass die Ausgabe gültig war.
Zwei weitere Beispiele für Unit-Tests, die von PyLith stammen , sind die Punktlokalisierung , bei der es sich um eine einzelne Funktion handelt, für die sich leicht synthetische Ergebnisse erzielen lassen, und die Erzeugung von kohäsiven Zellen mit null Volumen in einem Netz, das mehrere Funktionen umfasst, jedoch ein umschriebenes Teil von adressiert Funktionalität im Code.
Es gibt viele Tests dieser Art, einschließlich Konservierungs- und Konsistenztests. Die Operation unterscheidet sich nicht wesentlich von der Regression (Sie führen einen Test durch und vergleichen die Ausgabe mit einem Standard), die Standardausgabe stammt jedoch aus einer Spezifikation und nicht aus einem vorherigen Durchlauf.
quelle
Seit ich in Code Complete, 2. Ausgabe , über testgesteuerte Entwicklung gelesen habe, verwende ich ein Unit-Testing-FrameworkDies ist Teil meiner Entwicklungsstrategie und hat meine Produktivität erheblich gesteigert, da ich weniger Zeit für das Debuggen aufgewendet habe, da die verschiedenen Tests, die ich schreibe, diagnostisch sind. Als Nebeneffekt bin ich viel zuversichtlicher in meine wissenschaftlichen Ergebnisse und habe meine Unit-Tests mehrfach genutzt, um meine Ergebnisse zu verteidigen. Wenn bei einem Komponententest ein Fehler auftritt, kann ich normalerweise schnell herausfinden, warum. Wenn meine Anwendung abstürzt und alle Unit-Tests bestanden wurden, analysiere ich die Codeabdeckung, um festzustellen, welche Teile meines Codes nicht ausgeführt wurden, und gehe den Code mit einem Debugger durch, um die Fehlerquelle zu lokalisieren. Dann schreibe ich einen neuen Test, um sicherzustellen, dass der Fehler behoben bleibt.
Viele der Tests, die ich schreibe, sind keine reinen Unit-Tests. Streng definiert, sollen Unit-Tests die Funktionalität einer Funktion ausüben. Wenn ich einfach eine einzelne Funktion mit Scheindaten testen kann, mache ich das. Manchmal kann ich die Daten, die ich zum Schreiben eines Tests benötige, der die Funktionalität einer bestimmten Funktion ausübt, nicht einfach verspotten. Daher teste ich diese Funktion zusammen mit anderen in einem Integrationstest. IntegrationstestsTesten Sie das Verhalten mehrerer Funktionen gleichzeitig. Wie Matt betont, sind wissenschaftliche Codes oft eine Konstellation von ineinandergreifenden Funktionen, aber oft werden bestimmte Funktionen nacheinander aufgerufen, und es können Komponententests geschrieben werden, um die Ausgabe in Zwischenschritten zu testen. Wenn mein Produktionscode beispielsweise fünf Funktionen nacheinander aufruft, schreibe ich fünf Tests. Der erste Test ruft nur die erste Funktion auf (es handelt sich also um einen Komponententest). Dann ruft der zweite Test die erste und die zweite Funktion auf, der dritte Test ruft die ersten drei Funktionen auf und so weiter. Selbst wenn ich Unit-Tests für jede einzelne Funktion in meinem Code schreiben könnte, würde ich trotzdem Integrationstests schreiben, da Fehler auftreten können, wenn verschiedene modulare Teile eines Programms kombiniert werden. Nachdem ich alle Unit-Tests und Integrationstests geschrieben habe, von denen ich denke, dass ich sie brauche, habe ich Ich verpacke meine Fallstudien in Unit-Tests und verwende sie für Regressionstests, da ich möchte, dass meine Ergebnisse wiederholbar sind. Wenn sie nicht wiederholbar sind und ich unterschiedliche Ergebnisse erhalte, möchte ich wissen, warum. Das Scheitern eines Regressionstests ist vielleicht kein wirkliches Problem, aber es zwingt mich herauszufinden, ob die neuen Ergebnisse mindestens so zuverlässig sind wie die alten.
Neben Unit-Tests lohnen sich auch statische Codeanalysen, Speicher-Debugger und das Kompilieren mit Compiler-Warnflags, um einfache Fehler und nicht verwendeten Code abzufangen.
quelle
Meiner Erfahrung nach muss die Programmierung mit zunehmender Komplexität der Codes für wissenschaftliche Forschung sehr modular sein. Dies kann für Codes mit einer großen und alten Basis (
f77
irgendjemand?) Schmerzhaft sein, aber es ist notwendig, vorwärts zu gehen. Da ein Modul um einen bestimmten Aspekt des Codes herum aufgebaut wird (für CFD-Anwendungen, denken Sie an Randbedingungen oder Thermodynamik), sind Komponententests sehr wertvoll, um die neue Implementierung zu validieren und Probleme und weitere Softwareentwicklungen zu isolieren.Diese Einheitentests sollten eine Stufe unter der Codeüberprüfung liegen (kann ich die analytische Lösung meiner Wellengleichung wiederherstellen?) Und zwei Stufen unter der Codeüberprüfung liegen (kann ich die korrekten Spitzen-Effektivwerte in meinem turbulenten Rohrfluss vorhersagen), um einfach sicherzustellen, dass die Programmierung korrekt ist (Sind die Argumente richtig übergeben worden, zeigen die Zeiger auf das Richtige?) und "math" (diese Unterroutine berechnet den Reibungskoeffizienten. Wenn ich eine Menge von Zahlen eingebe und die Lösung von Hand berechne, ergibt die Routine dasselbe.) Ergebnis?) sind korrekt. Grundsätzlich eine Ebene über das hinausgehen, was die Compiler erkennen können, dh grundlegende Syntaxfehler.
Ich würde es auf jeden Fall für einige wichtige Module in Ihrer Anwendung empfehlen. Man muss sich jedoch darüber im Klaren sein, dass dies äußerst mühsam und zeitaufwendig ist. Wenn Sie nicht über unbegrenzte personelle Ressourcen verfügen, würde ich es nicht für 100% eines komplexen Codes empfehlen.
quelle
Unit-Tests für wissenschaftliche Codes sind aus verschiedenen Gründen nützlich.
Drei sind insbesondere:
Unit-Tests helfen anderen, die Einschränkungen Ihres Codes zu verstehen. Unit-Tests sind im Grunde genommen eine Form der Dokumentation.
Unit-Tests stellen sicher, dass eine einzelne Codeeinheit die richtigen Ergebnisse liefert, und stellen sicher, dass sich das Verhalten eines Programms nicht ändert, wenn die Details geändert werden.
Mit Unit-Tests können Sie Ihre Research-Codes einfacher modularisieren. Dies kann besonders wichtig sein, wenn Sie versuchen, Ihren Code auf eine neue Plattform auszurichten, z. B. wenn Sie ihn parallelisieren oder auf einem GPGPU-Computer ausführen möchten.
Unit-Tests geben Ihnen vor allem die Gewissheit, dass die Forschungsergebnisse, die Sie mit Ihren Codes erstellen, gültig und überprüfbar sind.
Ich stelle fest, dass Sie in Ihrer Frage Regressionstests erwähnen. In vielen Fällen werden Regressionstests durch die automatisierte, regelmäßige Ausführung von Komponententests und / oder Integrationstests (die prüfen, ob Codeteile in Kombination korrekt funktionieren) durchgeführt. Beim wissenschaftlichen Rechnen erfolgt dies häufig durch Vergleich der Ausgabe mit experimentellen Daten oder dem Ergebnisse früherer Programme, denen vertraut wird). Es hört sich so an, als würden Sie bereits Integrationstests oder Unit-Tests auf der Ebene großer komplexer Komponenten erfolgreich durchführen.
Ich würde sagen, dass es angesichts der zunehmenden Komplexität von Forschungscodes und der Abhängigkeit von den Codes und Bibliotheken anderer Personen wichtig ist, zu verstehen, wo der Fehler auftritt, wenn er auftritt. Durch Unit-Tests kann der Fehler viel einfacher lokalisiert werden.
Die Beschreibung, Nachweise und Verweise finden Sie in Abschnitt 7 "Plan for mistakes" (Plan für Fehler) des Artikels, den ich als Co-Autor für Best Practices for Scienti fi c Computing verfasst habe. Außerdem wird das ergänzende Konzept der defensiven Programmierung vorgestellt.
quelle
In meinen deal.II-Klassen unterrichte ich, dass Software, die keine Tests hat, nicht richtig funktioniert (und betone, dass ich absichtlich gesagt habe: " funktioniert nicht richtig", nicht " funktioniert möglicherweise nicht richtig).
Natürlich lebe ich nach dem Mantra - so läuft das Geschäft. Ich habe 2.500 Tests mit jedem Commit durchgeführt ;-)
Im Ernst, ich denke, Matt definiert die beiden Testklassen bereits gut. Wir schreiben Komponententests für das Zeug auf niedrigerer Ebene und es entwickelt sich auf natürliche Weise zu Regressionstests für das Zeug auf höherer Ebene. Ich glaube nicht, dass ich eine klare Grenze ziehen könnte, die unsere Tests von der einen oder anderen Seite trennen würde. Es gibt sicherlich viele, die die Grenze überschreiten, in der sich jemand die Ausgabe angeschaut hat und diese als weitgehend vernünftig ansieht (Komponententest?). ohne es bis auf die letzte Genauigkeit angeschaut zu haben (Regressionstest?).
quelle
Ja und nein. Sicherlich nicht geeignet für grundlegende Routinen des Basis-Toolsets, mit dem Sie Ihr Leben vereinfachen, z. B. Konvertierungsroutinen, Zeichenfolgenzuordnungen, grundlegende Physik und Mathematik usw. Wenn es um Berechnungsklassen oder -funktionen geht, können sie im Allgemeinen lange Laufzeiten erfordern Sie können es sogar vorziehen, sie als Funktionstests anstatt als Einheiten zu testen. Unittest und stress viel auch jene Klassen und Entitäten, deren Level und Verwendung sich stark verändern werden (zB zu Optimierungszwecken) oder deren interne Details aus irgendeinem Grund geändert werden. Das typischste Beispiel ist eine Klasse, die eine riesige Matrix umschließt, die von der Festplatte abgebildet wird.
quelle
Absolut!
Was, das reicht dir nicht?
In der wissenschaftlichen Programmierung entwickeln wir uns mehr als in jeder anderen Art auf der Grundlage des Versuchs, ein physikalisches System zu finden. Woher wissen Sie, ob Sie das anders gemacht haben als durch Testen? Überlegen Sie sich, wie Sie Ihren Code verwenden möchten, bevor Sie mit dem Codieren beginnen, und führen Sie einige Beispielläufe aus. Versuchen Sie, mögliche Randfälle aufzufangen. Tun Sie dies auf modulare Weise - zum Beispiel können Sie für ein neuronales Netzwerk eine Reihe von Tests für ein einzelnes Neuron und eine Reihe von Tests für ein vollständiges neuronales Netzwerk durchführen. Auf diese Weise können Sie beim Schreiben von Code sicherstellen, dass Ihr Neuron funktioniert, bevor Sie mit der Arbeit im Netzwerk beginnen. In solchen Phasen zu arbeiten bedeutet, dass Sie, wenn Sie auf ein Problem stoßen, nur die letzte 'Phase' des zu testenden Codes haben, die früheren Phasen bereits getestet wurden.
Sobald Sie die Tests abgeschlossen haben, müssen Sie den Code in einer anderen Sprache umschreiben (z. B. in CUDA konvertieren) oder sogar, wenn Sie ihn nur aktualisieren, haben Sie bereits die Testfälle und können sie verwenden, um sie zu erstellen Stellen Sie sicher, dass beide Versionen Ihres Programms gleich funktionieren.
quelle
Ja.
Die Idee, dass jeder Code ohne Unit-Tests geschrieben wird, ist ein Gräuel. Es sei denn, Sie beweisen, dass Ihr Code korrekt ist, und beweisen dann, dass der Beweis korrekt ist = P.
quelle
Ich würde diese Frage eher pragmatisch als dogmatisch angehen. Stellen Sie sich die Frage: "Was könnte in Funktion X schief gehen?" Stellen Sie sich vor, was mit der Ausgabe passiert, wenn Sie einige typische Fehler in den Code einfügen: einen falschen Präfaktor, einen falschen Index, ... und schreiben Sie dann Komponententests, die diese Art von Fehler wahrscheinlich erkennen. Wenn es für eine gegebene Funktion keine Möglichkeit gibt, solche Tests zu schreiben, ohne den Code der Funktion selbst zu wiederholen, dann tun Sie dies nicht - sondern denken Sie an Tests auf der nächsthöheren Ebene.
Ein viel wichtigeres Problem bei Komponententests (oder in der Tat bei allen Tests) im wissenschaftlichen Code ist der Umgang mit den Unsicherheiten der Gleitkomma-Arithmetik. Soweit ich weiß, gibt es noch keine guten allgemeinen Lösungen.
quelle
Tangurena tut mir leid - hier ist das Mantra "Ungetesteter Code ist gebrochener Code" und das kam vom Chef. Anstatt all die guten Gründe für Unit-Tests zu wiederholen, möchte ich nur ein paar Details hinzufügen.
quelle
Ich habe Unit-Tests für mehrere kleine Codes (dh für einzelne Programmierer) erfolgreich eingesetzt, einschließlich der dritten Version meines Dissertations-Analysecodes in der Teilchenphysik.
Die ersten beiden Versionen waren unter ihrem eigenen Gewicht und der Vervielfachung von Zusammenhängen zusammengebrochen.
Andere haben geschrieben, dass die Interaktion zwischen Modulen oft der Ort ist, an dem die wissenschaftliche Codierung bricht, und sie haben damit Recht. Es ist jedoch viel einfacher, diese Probleme zu diagnostizieren , wenn Sie schlüssig nachweisen können, dass jedes Modul das tut, was es soll.
quelle
Ein etwas anderer Ansatz, den ich bei der Entwicklung eines chemischen Lösers (für komplexe geologische Domänen) verwendet habe, war das, was man Unit Testing per Copy and Paste Snippet nennen kann .
Der Bau eines Testkabels für den ursprünglichen Code, der in einen großen Modellierer für chemische Systeme eingebettet war, war im Zeitrahmen nicht realisierbar.
Es gelang mir jedoch, eine immer komplexer werdende Sammlung von Ausschnitten zu erstellen, die die Funktionsweise des (Boost Spirit) -Parsers für die chemischen Formeln als Einheitentests für verschiedene Ausdrücke zeigten.
Der letzte, komplexeste Komponententest kam dem im System benötigten Code sehr nahe, ohne dass dieser Code geändert werden musste, um nachahmbar zu sein. Auf diese Weise konnte ich meinen einheitlich getesteten Code kopieren.
Was dies zu mehr als nur einer Lernübung und einer echten Regressionssuite macht, sind zwei Faktoren: Die Komponententests werden in der Hauptquelle gespeichert und als Teil anderer Tests für diese Anwendung ausgeführt (und ja, sie haben einen Nebeneffekt von Boost aufgefangen Änderung der Stimmung 2 Jahre später) - Da der in der realen Anwendung kopierte und eingefügte Code nur geringfügig geändert wurde, kann er Kommentare zu den Komponententests enthalten, um die Synchronität zu gewährleisten.
quelle
Für größere Codebasen sind Tests (nicht unbedingt Komponententests) für das High-Level-Zeug nützlich. Unit-Tests für einige einfachere Algorithmen sind ebenfalls nützlich, um sicherzustellen, dass Ihr Code keinen Unsinn macht, da Ihre Hilfsfunktion
sin
anstelle von verwendet wirdcos
.Für den gesamten Forschungscode ist es jedoch sehr schwierig, Tests zu schreiben und zu warten. Algorithmen sind in der Regel sehr umfangreich, ohne dass aussagekräftige Zwischenergebnisse vorliegen, die offensichtliche Tests erfordern und deren Ausführung oft lange dauert, bis ein Ergebnis vorliegt. Natürlich können Sie gegen Referenzläufe testen, die gute Ergebnisse lieferten, aber dies ist kein guter Test im Sinne eines Einheitentests.
Ergebnisse sind oft Annäherungen an die wahre Lösung. Während Sie Ihre einfachen Funktionen testen können, ob sie bis zu einem gewissen Epsilon genau sind, ist es sehr schwierig zu überprüfen, ob z. B. ein Ergebnisgitter korrekt ist oder nicht, was durch visuelle Inspektion durch den Benutzer (Sie) zuvor bewertet wurde.
In solchen Fällen haben automatisierte Tests oft ein zu hohes Kosten-Nutzen-Verhältnis. Ich empfehle etwas besseres: Testprogramme schreiben. Zum Beispiel habe ich ein mittelgroßes Python-Skript geschrieben, um Daten über Ergebnisse wie Histogramme der Kantengrößen und Winkel eines Netzes, Fläche des größten und kleinsten Dreiecks und deren Verhältnis usw. zu erstellen.
Ich kann es verwenden beide Eingangs- und Ausgangs Maschen während des normalen Betriebs zu beurteilen und nutzen es eine Plausibilitätsprüfung zu haben , nachdem ich den Algorithmus verändert. Wenn ich den Algorithmus ändere, weiß ich nicht immer, ob das neue Ergebnis besser ist, weil es oft kein absolutes Maß gibt, welche Approximation die beste ist. Aber wenn ich solche Metriken erstelle, kann ich einige Faktoren beschreiben, die besser sind wie "Die neue Variante hat irgendwann ein besseres Winkelverhältnis, aber eine schlechtere Konvergenzrate".
quelle