Gibt es einen Grund, warum Tests nicht in den Code geschrieben wurden, den sie testen?

91

Ich habe in letzter Zeit ein bisschen über Literate Programming gelesen und dachte darüber nach ... Gut geschriebene Tests, insbesondere BDD-Spezifikationen, können besser erklären, was Code tut als Prosa und haben den großen Vorteil von Überprüfung ihrer eigenen Genauigkeit.

Ich habe noch nie Tests gesehen, die in den Code geschrieben wurden, den sie testen. Liegt dies nur daran, dass Sprachen es nicht einfach machen, Anwendungen und Testcodes in derselben Quelldatei zu trennen (und niemand hat es leicht gemacht), oder gibt es einen eher grundsätzlichen Grund, warum Benutzer Testcode und Anwendungscode trennen?

Chris Devereux
quelle
33
Einige Programmiersprachen wie Python mit doctest ermöglichen dies.
Simon Bergot
2
Möglicherweise sind die BDD-Spezifikationen besser als die Prosa, um den Code zu erklären, aber das bedeutet nicht, dass die Kombination der beiden nicht besser ist.
JeffO
5
Die Hälfte der hier vorgebrachten Argumente gilt auch für die Inline-Dokumentation.
CodesInChaos
3
@ Simon Doctests sind zu simpel für ernsthafte Tests, vor allem, weil sie nicht dafür ausgelegt sind. Sie waren dafür gedacht, Codebeispiele in der Dokumentation zu haben, die automatisch überprüft werden können, und zeichnen sich dadurch aus. Nun, einige Leute verwenden sie auch für Unit-Tests, aber in letzter Zeit (wie in den letzten Jahren) hat dies eine Menge Flak gekostet, da es dazu neigt, in fragilen Chaos, übermäßig ausführlicher "Dokumentation" und anderen Chaos zu enden.
7
Design by Contract ermöglicht Inline-Spezifikationen, die das Testen vereinfachen.
Fuhrmanator

Antworten:

89

Der einzige Vorteil, den ich mir für Inline-Tests vorstellen kann, ist die Reduzierung der Anzahl der zu schreibenden Dateien. Bei modernen IDEs ist dies keine so große Sache.

Es gibt jedoch eine Reihe offensichtlicher Nachteile beim Inline-Testen:

  • Es verstößt gegen die Trennung von Bedenken . Dies mag umstritten sein, aber für mich ist das Testen von Funktionen eine andere Aufgabe als die Implementierung.
  • Sie müssten entweder neue Sprachfunktionen einführen, um zwischen Tests / Implementierung zu unterscheiden, oder Sie riskieren, die Grenze zwischen den beiden zu verwischen.
  • Größere Quelldateien sind schwerer zu bearbeiten: schwerer zu lesen, schwerer zu verstehen, es ist wahrscheinlicher, dass Sie mit Konflikten bei der Quellcodeverwaltung zu kämpfen haben.
  • Ich denke, es würde es schwieriger machen, sozusagen Ihren "Tester" -Hut aufzusetzen. Wenn Sie sich die Implementierungsdetails ansehen, werden Sie eher versucht sein, die Implementierung bestimmter Tests zu überspringen.
vaughandroid
quelle
9
Das ist interessant. Ich denke, der Vorteil, den ich sehen kann, ist, dass Sie, wenn Sie Ihren "Kodierer" aufgesetzt haben, über Tests nachdenken möchten, aber es ist ein guter Punkt, dass das Gegenteil nicht der Fall ist.
Chris Devereux
2
In diesem Sinne ist es möglich (und vielleicht wünschenswert), dass eine Person die Tests erstellt und eine zweite Person den Code tatsächlich implementiert. Das Inline-Setzen der Tests erschwert dies.
Jim Nutt
6
würde abstimmen, wenn ich könnte. Wie ist das überhaupt eine Antwort? Implementierer schreiben keine Tests? Leute, die Tests überspringen, wenn sie Implementierungsdetails betrachten? "Just too hard" Konflikte bei großen Dateien? Und wie in irgendeiner Weise könnte ein Test mit einem Implementierungsdetail verwechselt werden?
Bharal
5
@bharal Auch zu "Just too hard", Masochismus ist eine Tugend des Narren. Ich möchte, dass alles einfach ist, bis auf das Problem, das ich eigentlich zu lösen versuche.
Deworde
3
Unit-Test kann als Dokumentation angesehen werden. Aus dem gleichen Grund wie Kommentare sollten Unit-Tests in den Code aufgenommen werden, um die Lesbarkeit zu verbessern. Das Problem dabei ist jedoch, dass es in der Regel viele Komponententests gibt und viel Aufwand bei der Testimplementierung, der nicht die erwarteten Ergebnisse angibt. Auch Kommentare im Code sollten kurz und bündig gehalten werden, wobei größere Erklärungen aus dem Weg geräumt werden sollten - in einen Kommentarblock außerhalb der Funktion, in eine separate Datei oder möglicherweise in ein Designdokument. Unit-Tests sind IMO selten, wenn überhaupt kurz genug, um im getesteten Code wie Kommentare zu bleiben.
Steve314
36

Ich kann mir einige vorstellen:

  • Lesbarkeit. Das Einstreuen von "echtem" Code und Tests erschwert das Lesen des echten Codes.

  • Code aufblähen. Das Mischen von "echtem" Code und Testcode in dieselben Dateien / Klassen / was auch immer führt wahrscheinlich zu größeren kompilierten Dateien usw. Dies ist besonders wichtig für Sprachen mit später Bindung.

  • Möglicherweise möchten Sie nicht, dass Ihre Kunden / Kunden Ihren Testcode sehen. (Ich mag diesen Grund nicht ... aber wenn Sie an einem Closed-Source-Projekt arbeiten, ist es unwahrscheinlich, dass der Testcode dem Kunden trotzdem hilft.)

Jetzt gibt es mögliche Problemumgehungen für jedes dieser Probleme. Aber IMO, es ist einfacher, überhaupt nicht dorthin zu gehen.


Es ist erwähnenswert, dass Java-Programmierer in der Anfangszeit solche Dinge taten. zB Einbeziehen einer main(...)Methode in eine Klasse, um das Testen zu erleichtern. Diese Idee ist fast vollständig verschwunden. Es ist in der Industrie üblich, Tests separat mit einem Test-Framework durchzuführen.

Es ist auch erwähnenswert, dass die literarische Programmierung (wie von Knuth konzipiert) in der Softwareentwicklungsbranche noch nie Fuß gefasst hat.

Stephen C
quelle
4
+1 Lesbarkeitsprobleme - Testcode ist möglicherweise proportional zum Implementierungscode, insbesondere bei OO-Designs.
Fuhrmanator
2
+1 für Hinweise zur Verwendung von Testframeworks. Ich kann mir nicht vorstellen, ein gutes Testframework gleichzeitig mit Produktionscode zu verwenden.
Joshin4colours
1
RE: Möglicherweise möchten Sie nicht, dass Ihre Kunden / Kunden Ihren Testcode sehen. (Dieser Grund gefällt mir nicht ... aber wenn Sie an einem Closed-Source-Projekt arbeiten, hilft der Testcode dem Kunden wahrscheinlich sowieso nicht.) - Es kann wünschenswert sein, die Tests auf dem Client-Computer auszuführen. Das Ausführen der Tests kann dazu beitragen, das Problem schnell zu identifizieren und Unterschiede in der
Clientumgebung zu erkennen
1
@sixtyfootersdude - das ist eine ziemlich ungewöhnliche Situation. Und wenn Sie Closed Source entwickeln, möchten Sie Ihre Tests nicht in Ihre Standard-Binärdistribution aufnehmen, nur für den Fall. (Sie würden ein separates Bundle mit den Tests erstellen, die der Kunde ausführen soll.)
Stephen C
1
1) Haben Sie den ersten Teil meiner Antwort verpasst, in dem ich drei tatsächliche Gründe genannt habe? Da war ein "kritischer Gedanke" involviert ... 2) Haben Sie den zweiten Teil verpasst, in dem ich sagte, dass Java-Programmierer dies getan haben, aber sie tun es jetzt nicht? Und die offensichtliche Folgerung, dass Programmierer damit aufgehört haben ... aus gutem Grund?
Stephen C
14

Eigentlich können Sie sich Design By Contract so vorstellen. Das Problem ist, dass die meisten Programmiersprachen nicht zulassen, dass Sie Code wie folgt schreiben :( Es ist sehr einfach, die Voraussetzungen manuell zu testen, aber die Post-Bedingungen sind eine echte Herausforderung, ohne die Art und Weise zu ändern, in der Sie Code schreiben (eine riesige negative IMO).

Michael Feathers hat eine Präsentation darüber und dies ist eine der vielen Möglichkeiten, wie Sie die Codequalität verbessern können.

Daniel Kaplan
quelle
13

Aus den gleichen Gründen, aus denen Sie versuchen, eine enge Kopplung zwischen Klassen in Ihrem Code zu vermeiden, sollten Sie auch eine unnötige Kopplung zwischen Tests und Code vermeiden.

Erstellung: Tests und Code können zu unterschiedlichen Zeiten von unterschiedlichen Personen geschrieben werden.

Kontrolle: Wenn Tests verwendet werden, um Anforderungen zu spezifizieren, möchten Sie sicher, dass sie unterschiedlichen Regeln unterliegen, die festlegen, wer sie wann ändern kann.

Wiederverwendbarkeit: Wenn Sie die Tests inline setzen, können Sie sie nicht mit einem anderen Codeteil verwenden.

Stellen Sie sich vor, Sie haben einen Teil des Codes, der die Aufgabe korrekt erledigt, aber in Bezug auf Leistung, Wartbarkeit und was auch immer zu wünschen übrig lässt. Sie beschließen, diesen Code durch neuen und verbesserten Code zu ersetzen. Mit denselben Tests können Sie überprüfen, ob der neue Code dieselben Ergebnisse wie der alte Code liefert.

Auswählbarkeit: Wenn Sie die Tests vom Code trennen, können Sie leichter auswählen, welche Tests Sie ausführen möchten.

Beispielsweise verfügen Sie möglicherweise über eine kleine Testsuite, die sich nur auf den Code bezieht, an dem Sie gerade arbeiten, und eine größere Suite, die das gesamte Projekt testet.

Caleb
quelle
Ich bin verwirrt über Ihre Gründe: TDD sagt bereits, dass die Testerstellung vor (oder zur gleichen Zeit) wie der Produktionscode erfolgt und vom selben Programmierer durchgeführt werden muss! Sie weisen auch darauf hin, dass Tests den Anforderungen ziemlich ähnlich sind. Natürlich gelten diese Einwände nicht, wenn Sie das TDD-Dogma nicht abonnieren (was akzeptabel wäre, aber Sie müssen es klarstellen!). Was genau ist ein "wiederverwendbarer" Test? Sind Tests nicht per Definition spezifisch für den Code, den sie testen?
Andres F.
1
@AndresF. Nein, Tests sind nicht spezifisch für den Code, den sie testen. Sie sind spezifisch für das Verhalten, auf das sie testen. Nehmen wir also an, Sie haben ein Widget-Modul mit einer Reihe von Tests, die sicherstellen, dass sich Widget korrekt verhält. Ihr Kollege hat BetterWidget entwickelt, das das Gleiche wie Widget tut, aber dreimal schneller ist. Wenn die Tests für Widget in der gleichen Weise in den Quellcode des Widgets eingebettet sind, wie Literate Programming die Dokumentation in den Quellcode einbettet, können Sie diese Tests nicht sehr gut auf BetterWidget anwenden, um zu überprüfen, ob sie sich so verhalten wie Widget.
Caleb
@AndresF. Sie müssen nicht angeben, dass Sie TDD nicht folgen. Es ist keine kosmische Vorgabe. Was den Wiederverwendungspunkt betrifft. Beim Testen eines Systems kümmern Sie sich um die Ein- und Ausgänge, nicht um die Interna. Wenn Sie dann ein neues System erstellen müssen, das sich genauso verhält, aber anders implementiert ist, ist es großartig, Tests zu haben, die Sie sowohl auf dem alten als auch auf dem neuen System ausführen können. Das ist mir mehrmals passiert, manchmal muss man am neuen System arbeiten, während das alte noch in Arbeit ist, oder sie sogar nebeneinander ausführen. Schauen Sie sich an, wie Facebook mit den Reaktionstests "Reactive Fibre" getestet hat, um zur Parität zu gelangen.
user1852503
10

Hier sind einige zusätzliche Gründe, die mir einfallen:

  • Tests in einer separaten Bibliothek zu haben, macht es einfacher, nur diese Bibliothek mit Ihrem Testframework und nicht mit Ihrem Produktionscode zu verknüpfen (dies könnte von einem Vorprozessor vermieden werden, aber warum sollte man so etwas erstellen, wenn die einfachere Lösung darin besteht, die Tests einzuschreiben? ein separater Ort)

  • Tests einer Funktion, einer Klasse oder einer Bibliothek werden normalerweise unter dem Gesichtspunkt "Benutzer" (ein Benutzer dieser Funktion / Klasse / Bibliothek) geschrieben. Solch ein "Verwenden von Code" wird normalerweise in eine separate Datei oder Bibliothek geschrieben, und ein Test kann klarer oder "realistischer" sein, wenn er diese Situation nachahmt.

Doc Brown
quelle
5

Wenn die Tests inline wären, müssten Sie den Code entfernen, den Sie zum Testen benötigen, wenn Sie das Produkt an Ihren Kunden senden. Ein zusätzlicher Ort, an dem Sie Ihre Tests speichern, trennt einfach zwischen dem Code, den Sie benötigen, und dem Code, den Ihr Kunde benötigt.

mhr
quelle
9
Nicht unmöglich. Es würde eine zusätzliche Vorverarbeitungsphase erfordern, genau wie bei LP. Dies kann beispielsweise problemlos in C oder in einer Compile-to-JS-Sprache erfolgen.
Chris Devereux
+1 für den Hinweis auf mich. Ich habe meine Antwort bearbeitet, um das darzustellen.
25.
Es gibt auch die Annahme, dass die Codegröße in jedem Fall von Bedeutung ist. Nur weil es in einigen Fällen wichtig ist, heißt das nicht, dass es in allen Fällen wichtig ist. Es gibt viele Umgebungen, in denen Programmierer nicht darauf bedacht sind, die Quellcodegröße zu optimieren. Wenn das der Fall wäre, würden sie nicht so viele Klassen schaffen.
Zumalifeguard
5

Diese Idee läuft einfach auf eine "Self_Test" -Methode im Kontext eines objektbasierten oder objektorientierten Designs hinaus. Wenn Sie eine kompilierte objektbasierte Sprache wie Ada verwenden, wird der gesamte Selbsttestcode während der Produktionskompilierung vom Compiler als nicht verwendet (nie aufgerufen) markiert und daher alles wegoptimiert - nichts davon erscheint in der resultierende ausführbare Datei.

Die Verwendung einer "Self_Test" -Methode ist eine sehr gute Idee, und wenn Programmierer wirklich auf Qualität bedacht wären, würden sie es alle tun. Ein wichtiges Problem ist jedoch, dass die Methode "Self_Test" eine intensive Disziplin haben muss, da sie nicht auf Implementierungsdetails zugreifen kann und stattdessen nur auf alle anderen veröffentlichten Methoden innerhalb der Objektspezifikation angewiesen ist. Wenn der Selbsttest fehlschlägt, muss sich die Implementierung natürlich ändern. Der Selbsttest sollte streng alle veröffentlichten Eigenschaften der Objektmethoden testen, sich jedoch niemals auf Details einer bestimmten Implementierung verlassen.

Objektbasierte und objektorientierte Sprachen bieten häufig genau diese Art von Disziplin in Bezug auf Methoden außerhalb des getesteten Objekts (sie erzwingen die Objektspezifikation, verhindern den Zugriff auf die Implementierungsdetails und verursachen einen Kompilierungsfehler, wenn ein solcher Versuch entdeckt wird ). Alle internen Methoden des Objekts erhalten jedoch vollständigen Zugriff auf alle Implementierungsdetails. Die Selbsttestmethode befindet sich in einer einzigartigen Situation: Sie muss aufgrund ihrer Natur eine interne Methode sein (Selbsttest ist offensichtlich eine Methode des zu testenden Objekts), muss jedoch die gesamte Compiler-Disziplin einer externen Methode enthalten ( Es muss unabhängig von den Implementierungsdetails des Objekts sein. Nur wenige Programmiersprachen bieten die Möglichkeit, ein Objekt zu disziplinieren. “ s interne Methode als wäre es eine externe Methode. Dies ist also ein wichtiges Problem beim Design von Programmiersprachen.

Wenn die Programmiersprache nicht ordnungsgemäß unterstützt wird, erstellen Sie am besten ein Begleitobjekt. Mit anderen Worten, für jedes Objekt, das Sie codieren (nennen wir es "Big_Object"), erstellen Sie auch ein zweites Begleitobjekt, dessen Name aus einem Standardsuffix besteht, das mit dem Namen des "echten" Objekts verknüpft ist (in diesem Fall "Big_Object_Self_Test") ") und deren Spezifikation aus einer einzelnen Methode besteht (" Big_Object_Self_Test.Self_Test (This_Big_Object: Big_Object) return Boolean; "). Das Begleitobjekt hängt dann von der Spezifikation des Hauptobjekts ab, und der Compiler setzt die gesamte Disziplin dieser Spezifikation vollständig gegen die Implementierung des Begleitobjekts durch.

commenter8
quelle
4

Dies ist eine Reaktion auf eine große Anzahl von Kommentaren, die darauf hindeuten, dass Inline-Tests nicht durchgeführt werden, da es schwierig bis unmöglich ist, den Testcode aus Release-Builds zu entfernen. Das ist falsch. Nahezu alle Compiler und Assembler unterstützen dies bereits, bei kompilierten Sprachen wie C, C ++, C # geschieht dies mit sogenannten Compiler-Direktiven.

Im Fall von c # (ich glaube auch, dass die Syntax in c ++ je nachdem, welchen Compiler Sie verwenden, leicht unterschiedlich sein kann) können Sie dies auf diese Weise tun.

#define DEBUG //  = true if c++ code
#define TEST /* can also be defined in the make file for c++ or project file for c# and applies to all associated .cs/.cpp files */

//somewhere in your code
#if DEBUG
// debug only code
#elif TEST
// test only code
#endif

Da dies Compiler-Direktiven verwendet, ist der Code in den ausführbaren Dateien, die erstellt werden, nicht vorhanden, wenn die Flags nicht gesetzt sind. Auf diese Weise erstellen Sie auch Programme zum einmaligen Schreiben und zum zweimaligen Kompilieren für mehrere Plattformen / Hardware.

John
quelle
2

Wir verwenden Inline-Tests mit unserem Perl-Code. Es gibt ein Modul, Test :: Inline , das aus dem Inline-Code Testdateien generiert.

Ich bin nicht besonders gut darin, meine Tests zu organisieren, und habe festgestellt, dass sie einfacher sind und eher gewartet werden, wenn sie inline sind.

Als Antwort auf einige der angesprochenen Bedenken:

  • Die eingebetteten Tests sind in POD-Abschnitten geschrieben, sodass sie nicht Teil des eigentlichen Codes sind. Sie werden vom Interpreter ignoriert, sodass sich kein Code aufbläht.
  • Wir verwenden Vim-Falzen , um die Testabschnitte zu verbergen. Das einzige, was Sie sehen, ist eine einzelne Linie über jeder Methode, wie sie getestet wird +-- 33 lines: #test----. Wenn Sie mit dem Test arbeiten möchten, erweitern Sie ihn einfach.
  • Das Test :: Inline-Modul "kompiliert" die Tests zu normalen TAP-kompatiblen Dateien, sodass sie mit herkömmlichen Tests koexistieren können.

Als Referenz:

mla
quelle
1

Erlang 2 unterstützt tatsächlich Inline-Tests. Jeder boolesche Ausdruck im Code, der nicht verwendet wird (z. B. einer Variablen zugewiesen oder übergeben), wird automatisch als Test behandelt und vom Compiler ausgewertet. Wenn der Ausdruck falsch ist, wird der Code nicht kompiliert.

Mark Rendle
quelle
1

Ein weiterer Grund für die Trennung von Tests ist, dass Sie häufig zusätzliche oder sogar andere Bibliotheken zum Testen verwenden als für die eigentliche Implementierung. Wenn Sie Tests und Implementierung mischen, kann der Compiler die versehentliche Verwendung von Testbibliotheken in der Implementierung nicht abfangen.

Außerdem enthalten Tests in der Regel viel mehr Codezeilen als die von ihnen getesteten Implementierungsteile, sodass Sie zwischen allen Tests Schwierigkeiten haben, die Implementierung zu finden. :-)

Hans-Peter Störr
quelle
0

Das stimmt nicht. Es ist viel besser, Ihre Unit-Tests neben dem Produktionscode zu platzieren, wenn der Produktionscode rein ist, insbesondere wenn die Produktionsroutine rein ist.

Wenn Sie beispielsweise unter .NET entwickeln, können Sie Ihren Testcode in die Produktionsassembly einfügen und diese vor dem Versand mit Scalpel entfernen.

Zumalifeguard
quelle