Ist testbarer Code besserer Code?

103

Ich habe versucht, mir angewöhnen, regelmäßig Unit-Tests mit meinem Code zu schreiben , aber ich habe gelesen, dass es zuerst wichtig ist, testbaren Code zu schreiben . Diese Frage berührt die SOLID-Prinzipien des Schreibens von testbarem Code, aber ich möchte wissen, ob diese Designprinzipien nützlich (oder zumindest nicht schädlich) sind, ohne überhaupt Tests schreiben zu wollen. Zur Klarstellung: Ich verstehe, wie wichtig es ist, Tests zu schreiben. Dies ist keine Frage ihrer Nützlichkeit.

Um meine Verwirrung zu veranschaulichen, gibt der Autor in dem Stück , das diese Frage inspiriert hat, ein Beispiel für eine Funktion an, die die aktuelle Uhrzeit überprüft und je nach Uhrzeit einen bestimmten Wert zurückgibt. Der Autor weist darauf hin, dass der Code fehlerhaft ist, da er die Daten (die Zeit) erzeugt, die er intern verwendet, was das Testen erschwert. Für mich scheint es jedoch übertrieben, die Zeit als Argument zu vergehen. Irgendwann muss der Wert initialisiert werden, und warum nicht am nächsten am Verbrauch? Außerdem ist der Zweck der Methode in meinem Kopf, einen Wert basierend auf der aktuellen Zeit zurückzugeben , indem Sie ihn zu einem Parameter machen, der impliziert, dass dieser Zweck geändert werden kann / sollte. Diese und andere Fragen führen mich zu der Frage, ob testbarer Code gleichbedeutend mit "besserem" Code ist.

Ist das Schreiben von testbarem Code auch dann noch eine gute Übung, wenn keine Tests durchgeführt wurden?


Ist testbarer Code tatsächlich stabiler? wurde als Duplikat vorgeschlagen. Bei dieser Frage geht es jedoch um die "Stabilität" des Codes, aber ich frage mich allgemeiner, ob der Code auch aus anderen Gründen überlegen ist, z. B. aus Gründen der Lesbarkeit, Leistung, Kopplung usw.

WannabeCoder
quelle
24
Es gibt eine spezielle Eigenschaft der Funktion, die es erfordert, dass Sie die Zeit mit dem Namen idempotency verstreichen lassen . Eine solche Funktion führt bei jedem Aufruf mit einem bestimmten Argumentwert zu demselben Ergebnis , was sie nicht nur testbarer, sondern auch komponierbarer und nachvollziehbarer macht.
Robert Harvey
4
Können Sie "besseren Code" definieren? Meinen Sie "wartbar" ?, "einfacher-zu-verwenden-ohne-IOC-Container-Magic"?
k3b
7
Ich vermute, Sie hatten noch nie einen Testfehler, da die tatsächliche Systemzeit und dann der Zeitzonenversatz geändert wurden.
Andy
5
Es ist besser als nicht testbarer Code.
Tulains Córdova
14
@RobertHarvey Ich würde nicht so Idempotenz nennen, würde ich sagen , es ist referentielle Transparenz : Wenn func(X)Rückkehr "Morning", dann alle Vorkommen zu ersetzen , func(X)mit "Morning"wird das Programm nicht ändern (dh Berufung. funcTut nichts anderes als den Wert zurück). Idempotency impliziert entweder, dass func(func(X)) == X(die nicht func(X); func(X);func(X)
typenrichtig

Antworten:

116

In Bezug auf die gemeinsame Definition von Unit-Tests würde ich Nein sagen. Ich habe gesehen, dass einfacher Code aufgrund der Notwendigkeit, ihn an das Testframework anzupassen, verwickelt ist (z. B. Schnittstellen und IoC überall, was es schwierig macht, die Dinge durch Schichten von Schnittstellenaufrufen und Daten zu verfolgen, die offensichtlich von der Magie übergeben werden sollten). Angesichts der Wahl zwischen einfach zu verstehendem Code oder einfach zu testendem Code entscheide ich mich jedes Mal für den wartbaren Code.

Dies bedeutet nicht, nicht zu testen, sondern die Werkzeuge an Ihre Bedürfnisse anzupassen, nicht umgekehrt. Es gibt andere Möglichkeiten zum Testen (aber schwer verständlicher Code ist immer schlechter Code). Sie können beispielsweise weniger detaillierte Komponententests erstellen (z. B. die Einstellung von Martin Fowler , dass eine Einheit im Allgemeinen eine Klasse und keine Methode ist) oder Ihr Programm stattdessen mit automatisierten Integrationstests testen. Dies mag nicht so hübsch sein, wie Ihr Test-Framework mit grünen Häkchen aufleuchtet, aber wir suchen nach getestetem Code, nicht nach der Gamifizierung des Prozesses, oder?

Sie können dafür sorgen, dass Ihr Code einfach zu warten ist und dennoch für Komponententests geeignet ist, indem Sie gute Schnittstellen zwischen ihnen definieren und dann Tests schreiben, die die öffentliche Schnittstelle der Komponente ausüben. oder Sie könnten ein besseres Testframework erhalten (eines, das Funktionen zur Laufzeit ersetzt, um sie zu verspotten, anstatt dass der Code mit vorhandenen Verspottungen kompiliert werden muss). Mit einem besseren Unit-Test-Framework können Sie die GetCurrentTime () -Funktionalität des Systems zur Laufzeit durch Ihre eigene ersetzen, sodass Sie keine künstlichen Wrapper einführen müssen, nur um dem Test-Tool zu entsprechen.

gbjbaanb
quelle
3
Kommentare sind nicht für eine längere Diskussion gedacht. Diese Unterhaltung wurde in den Chat verschoben .
Welt Ingenieur
2
Ich denke, es ist erwähnenswert, dass ich mindestens eine Sprache kenne, mit der Sie das tun können, was in Ihrem letzten Absatz beschrieben wird: Python mit Mock. Aufgrund der Art und Weise, wie Modulimporte funktionieren, kann so ziemlich alles außer Schlüsselwörtern durch einen Schein ersetzt werden, sogar Standard-API-Methoden / Klassen / etc. Das ist also möglich, aber es kann erforderlich sein, die Sprache so zu gestalten, dass diese Art von Flexibilität unterstützt wird.
jpmc26
5
Ich denke, es gibt einen Unterschied zwischen "testbarem Code" und "Code [verdreht], um dem Test-Framework zu entsprechen". Ich bin mir nicht sicher, wohin ich mit diesem Kommentar gehe, außer zu sagen, dass "verdrehter" Code schlecht und "testbarer" Code mit guten Schnittstellen gut ist.
Bryan Oakley
2
Ich habe einige meiner Gedanken in den Kommentaren zum Ausdruck gebracht (da hier keine erweiterten Kommentare erlaubt sind). Um klar zu sein: Ich bin der Autor des genannten Artikels :)
Sergey Kolodiy
Ich muss @BryanOakley zustimmen. "Testbarer Code" deutet darauf hin, dass Ihre Bedenken getrennt sind: Es ist möglich, einen Aspekt (ein Modul) ohne Beeinflussung durch andere Aspekte zu testen. Ich würde sagen, dies unterscheidet sich von "Anpassen Ihrer projektunterstützungsspezifischen Testkonventionen". Dies ist ähnlich wie bei Entwurfsmustern: Sie sollten nicht gezwungen werden. Code, der ordnungsgemäß Entwurfsmuster verwendet, wird als starker Code betrachtet. Gleiches gilt für Prüfgrundsätze. Wenn Sie Ihren Code "testbar" machen, wird der Code Ihres Projekts übermäßig verdreht, und Sie machen etwas falsch.
Vince Emigh
68

Ist das Schreiben von testbarem Code auch dann noch eine gute Übung, wenn keine Tests durchgeführt wurden?

Zunächst einmal ist das Fehlen von Tests ein weitaus größeres Problem, als dass Ihr Code testbar ist oder nicht. Wenn Sie keine Komponententests haben, sind Sie mit Ihrem Code / Ihrer Funktion noch nicht fertig.

Aus dem Weg, würde ich nicht sagen, dass es wichtig ist, testbaren Code zu schreiben - es ist wichtig, flexiblen Code zu schreiben . Unflexibler Code ist schwer zu testen, daher gibt es viele Überschneidungen bei der Vorgehensweise und dem, was die Leute ihn nennen.

Für mich gibt es beim Schreiben von Code immer eine Reihe von Prioritäten:

  1. Lass es funktionieren - wenn der Code nicht das tut, was er tun muss, ist er wertlos.
  2. Wartungsfähig machen - Wenn der Code nicht gewartet werden kann, funktioniert er schnell nicht mehr.
  3. Flexibel machen - Wenn der Code nicht flexibel ist, funktioniert er nicht mehr, wenn das Geschäft unweigerlich vorbeikommt, und fragt, ob der Code XYZ kann.
  4. Machen Sie es schnell - jenseits eines akzeptablen Grundniveaus ist Leistung nur noch Sauce.

Unit-Tests helfen, den Code zu warten, aber nur bis zu einem gewissen Punkt. Wenn Sie den Code weniger lesbar oder anfälliger machen, damit die Komponententests funktionieren, wird dies kontraproduktiv. "Testbarer Code" ist im Allgemeinen flexibler Code, das ist gut, aber nicht so wichtig wie Funktionalität oder Wartbarkeit. Für etwas wie die aktuelle Zeit ist es gut, dies flexibel zu machen, aber es beeinträchtigt die Wartbarkeit, indem es schwieriger wird, den Code richtig und komplexer zu verwenden. Da die Wartbarkeit wichtiger ist, irre ich mich in der Regel in Richtung eines einfacheren Ansatzes, auch wenn dieser weniger testbar ist.

Telastyn
quelle
4
Ich mag die Beziehung, die Sie zwischen prüfbar und flexibel hervorheben - das macht das ganze Problem für mich verständlicher. Flexibilität ermöglicht die Anpassung Ihres Codes, macht ihn jedoch notwendigerweise etwas abstrakter und weniger intuitiv zu verstehen, aber das ist ein lohnendes Opfer für die Vorteile.
WannabeCoder
3
Allerdings sehe ich häufig Methoden, die privat sein sollten, als öffentlich oder auf Paketebene, damit das Unit-Testing-Framework direkt auf sie zugreifen kann. Weit entfernt von einem idealen Ansatz.
Jwenting
4
@WannabeCoder Natürlich lohnt es sich nur, Flexibilität hinzuzufügen, wenn Sie am Ende Zeit sparen. Aus diesem Grund schreiben wir nicht jede einzelne Methode gegen eine Schnittstelle - meistens ist es nur einfacher, den Code neu zu schreiben, als von Anfang an zu viel Flexibilität einzubeziehen. YAGNI ist immer noch ein äußerst leistungsfähiges Prinzip - stellen Sie nur sicher, dass Sie, wenn Sie es rückwirkend hinzufügen, im Durchschnitt nicht mehr Arbeit haben, als wenn Sie es vorzeitig implementieren. Es ist der Code, der YAGNI nicht folgt, der meiner Erfahrung nach die meisten Probleme mit der Flexibilität hat.
Luaan
3
"Wenn Sie keine Komponententests haben, ist Ihr Code / Ihre Funktion noch nicht fertig" - Unwahr. Die "Definition von erledigt" entscheidet das Team. Es kann einen gewissen Grad an Testabdeckung enthalten oder auch nicht. Aber nirgendwo gibt es eine strikte Anforderung, die besagt, dass ein Feature nicht "fertig" sein kann, wenn es keine Tests dafür gibt. Das Team kann entscheiden, ob Tests erforderlich sind oder nicht.
Donnerstag,
3
@Telastyn In über 10 Jahren Entwicklung hatte ich nie ein Team, das ein Unit-Testing-Framework vorgeschrieben hatte, und nur zwei, die sogar eines hatten (beide hatten eine schlechte Abdeckung). Ein Ort erforderte ein Word-Dokument zum Testen der Funktion, die Sie geschrieben haben. Das ist es. Vielleicht habe ich Pech? Ich bin kein Anti-Unit-Test (im Ernst, ich modifiziere die SQA.SE-Site, ich bin ein sehr pro-Unit-Test!), Aber ich habe nicht festgestellt, dass sie so weit verbreitet sind, wie von Ihrer Aussage behauptet.
corsiKa
50

Ja, das ist eine gute Übung. Der Grund dafür ist, dass die Testbarkeit nicht aus Gründen der Testbarkeit erfolgt. Es dient der Klarheit und Verständlichkeit, die es mit sich bringt.

Die Tests selbst interessieren niemanden. Es ist eine traurige Tatsache, dass wir große Regressionstestsuiten benötigen, weil wir nicht brillant genug sind, um perfekten Code zu schreiben, ohne ständig unseren Stand zu überprüfen. Wenn wir könnten, wäre das Konzept der Tests unbekannt, und all dies wäre kein Problem. Ich wünschte, ich könnte. Die Erfahrung hat jedoch gezeigt, dass fast jeder von uns dies nicht kann. Daher sind Tests, die unseren Code abdecken, eine gute Sache, auch wenn sie die Zeit für das Schreiben von Geschäftscode verkürzen.

Inwiefern verbessern Tests unseren Geschäftscode unabhängig vom eigentlichen Test? Indem wir gezwungen werden, unsere Funktionalität in Einheiten zu unterteilen, deren Richtigkeit leicht nachgewiesen werden kann. Diese Einheiten sind auch einfacher zu finden als diejenigen, zu denen wir sonst versucht wären zu schreiben.

Ihr Zeitbeispiel ist ein guter Punkt. Solange Sie nur eine Funktion haben, die die aktuelle Zeit zurückgibt, könnte es sinnlos sein, sie programmierbar zu machen. Wie schwer kann es sein, das in Ordnung zu bringen? Aber zwangsläufig Ihr Programm verwendet diese Funktion in anderem Code, und Sie wollen auf jeden Fall prüfen , dass unter verschiedenen Bedingungen Code, zu unterschiedlichen Zeiten mit. Daher ist es eine gute Idee, die Zeit zu manipulieren, die Ihre Funktion zurückgibt - nicht weil Sie Ihrem einzeiligen currentMillis()Anruf misstrauen , sondern weil Sie die Anrufer dieses Anrufs unter kontrollierten Umständen überprüfen müssen . Wie Sie sehen, ist es nützlich, Code testbar zu haben, auch wenn er für sich genommen nicht so viel Aufmerksamkeit verdient.

Kilian Foth
quelle
Ein anderes Beispiel ist, wenn Sie einen Teil des Codes eines Projekts an einen anderen Ort ziehen möchten (aus welchem ​​Grund auch immer). Je unabhängiger die verschiedenen Teile der Funktionalität voneinander sind, desto einfacher ist es, genau die Funktionalität zu extrahieren, die Sie benötigen, und nicht mehr.
Valentinstag
10
Nobody cares about the tests themselves-- Ich mache. Ich finde Tests eine bessere Dokumentation dessen, was der Code tut, als Kommentare oder Readme-Dateien.
jcollum
Ich lese schon seit einiger Zeit langsam über Testpraktiken (als jemand, der noch keine Einheitentests durchführt) und ich muss sagen, der letzte Teil über die Überprüfung des Anrufs unter kontrollierten Umständen und der flexiblere Code, der mitgeliefert wird es hat alle möglichen Dinge zum Einrasten gebracht. Danke.
plast1k
12

Irgendwann muss der Wert initialisiert werden, und warum nicht am nächsten am Verbrauch?

Möglicherweise müssen Sie diesen Code mit einem anderen Wert als dem intern generierten wiederverwenden. Die Möglichkeit, den Wert, den Sie als Parameter verwenden möchten, einzufügen, stellt sicher, dass Sie diese Werte zu jedem beliebigen Zeitpunkt generieren können, nicht nur "jetzt" (wobei "jetzt" bedeutet, wenn Sie den Code aufrufen).

Um Code tatsächlich testbar zu machen, muss Code erstellt werden, der (von Anfang an) in zwei verschiedenen Szenarien (Produktion und Test) verwendet werden kann.

Grundsätzlich kann man behaupten, dass es keinen Anreiz gibt, den Code in Abwesenheit von Tests testbar zu machen, aber es ist ein großer Vorteil, wiederverwendbaren Code zu schreiben, und die beiden sind Synonyme.

Außerdem ist der Zweck der Methode in meinem Kopf, einen Wert basierend auf der aktuellen Zeit zurückzugeben, indem Sie ihn zu einem Parameter machen, der impliziert, dass dieser Zweck geändert werden kann / sollte.

Sie könnten auch argumentieren, dass der Zweck dieser Methode darin besteht, einen Wert basierend auf einem Zeitwert zurückzugeben, und dass Sie diesen Wert basierend auf "now" generieren müssen. Eine davon ist flexibler, und wenn Sie sich mit der Zeit an die Auswahl dieser Variante gewöhnt haben, steigt die Rate der Wiederverwendung von Code.

utnapistim
quelle
10

Es mag albern erscheinen, es so zu sagen, aber wenn Sie Ihren Code testen möchten, ist es besser, testbaren Code zu schreiben. Du fragst:

Irgendwann muss der Wert initialisiert werden, und warum nicht am nächsten am Verbrauch?

Genau deshalb, weil dieser Code in dem Beispiel, auf das Sie sich beziehen, nicht mehr testbar ist. Es sei denn, Sie führen nur eine Teilmenge Ihrer Tests zu verschiedenen Tageszeiten durch. Oder Sie setzen die Systemuhr zurück. Oder eine andere Problemumgehung. Das alles ist schlimmer als nur Ihren Code flexibel zu machen.

Diese kleine Methode ist nicht nur unflexibel, sondern hat auch zwei Aufgaben: (1) Abrufen der Systemzeit und (2) Zurückgeben eines darauf basierenden Werts.

public static string GetTimeOfDay()
{
    DateTime time = DateTime.Now;
    if (time.Hour >= 0 && time.Hour < 6)
    {
        return "Night";
    }
    if (time.Hour >= 6 && time.Hour < 12)
    {
        return "Morning";
    }
    if (time.Hour >= 12 && time.Hour < 18)
    {
        return "Afternoon";
    }
    return "Evening";
}

Es ist sinnvoll, die Verantwortlichkeiten weiter aufzuteilen, damit der Teil außerhalb Ihrer Kontrolle ( DateTime.Now) den Rest Ihres Codes am wenigsten beeinflusst. Auf diese Weise wird der oben genannte Code einfacher, und der Nebeneffekt ist, dass er systematisch getestet werden kann.

Eric King
quelle
1
Sie müssen also früh morgens testen, um zu überprüfen, ob Sie das Ergebnis "Nacht" erhalten, wenn Sie es möchten. Das ist schwierig. Angenommen, Sie möchten überprüfen, ob das Datums-Handling am 29. Februar 2016 korrekt ist. Und einige iOS-Programmierer (und wahrscheinlich auch andere) leiden unter einem Anfängerfehler, der kurz vor oder nach Jahresbeginn die Sache durcheinanderbringt. Wie geht es Ihnen? Test dafür. Und aus Erfahrung würde ich die
Terminabwicklung
1
@ gnasher729 Genau mein Punkt. "Den Code testbar machen" ist eine einfache Änderung, die viele (Test-) Probleme lösen kann. Wenn Sie das Testen nicht automatisieren möchten, ist der Code vermutlich so, wie er ist, passierbar. Aber es wäre besser, wenn es "testbar" wäre.
Eric King
9

Es hat sicherlich Kosten, aber einige Entwickler sind es so gewohnt, sie zu bezahlen, dass sie vergessen haben, dass die Kosten da sind. In Ihrem Beispiel haben Sie jetzt zwei Einheiten anstelle von einer. Sie benötigen den aufrufenden Code, um eine zusätzliche Abhängigkeit zu initialisieren und zu verwalten. Auch wenn dies GetTimeOfDaytestbarer ist, sind Sie gleich wieder im selben Boot und testen Ihre neue IDateTimeProvider. Wenn Sie gute Tests haben, überwiegen die Vorteile normalerweise die Kosten.

Bis zu einem gewissen Grad ermutigt Sie das Schreiben von testbarem Code, Ihren Code auf eine wartbarere Art und Weise zu entwerfen. Der neue Code für das Abhängigkeitsmanagement ist ärgerlich. Sie sollten daher alle Ihre zeitabhängigen Funktionen zusammenfassen, wenn dies möglich ist. Dies kann helfen, Fehler zu minimieren und zu beheben, z. B. wenn Sie eine Seite direkt an einer Zeitgrenze laden und einige Elemente mit der Vorher-Zeit und andere mit der Nachher-Zeit rendern. Sie können Ihr Programm auch beschleunigen, indem Sie wiederholte Systemaufrufe vermeiden, um die aktuelle Uhrzeit abzurufen.

Natürlich sind diese architektonischen Verbesserungen in hohem Maße davon abhängig, dass jemand die Chancen wahrnimmt und umsetzt. Eine der größten Gefahren bei der Fokussierung auf Einheiten besteht darin, das Gesamtbild aus den Augen zu verlieren.

In vielen Unit-Test-Frameworks können Sie ein Mock-Objekt zur Laufzeit mit Affen patchen, wodurch Sie die Vorteile der Testbarkeit ohne all das Durcheinander nutzen können. Ich habe es sogar in C ++ gesehen. Sehen Sie sich diese Fähigkeit in Situationen an, in denen sich die Kosten für die Testbarkeit nicht lohnen.

Karl Bielefeldt
quelle
+1 - Sie müssen Design und Architektur verbessern, um das Schreiben von Komponententests zu vereinfachen.
BЈовић
3
+ - Auf die Architektur Ihres Codes kommt es an. Einfacheres Testen ist nur ein erfreulicher Nebeneffekt.
Gbjbaanb
8

Es ist möglich, dass nicht jedes Merkmal, das zur Testbarkeit beiträgt, außerhalb des Kontextes der Testbarkeit wünschenswert ist - ich habe Probleme, eine nicht testbezogene Begründung für den von Ihnen angegebenen Zeitparameter zu finden -, aber allgemein gesagt, die Merkmale, die zur Testbarkeit beitragen auch unabhängig von der Testbarkeit zu einem guten Code beitragen.

Im Großen und Ganzen ist testbarer Code formbarer Code. Es besteht aus kleinen, diskreten, zusammenhängenden Stücken, sodass einzelne Teile zur Wiederverwendung aufgerufen werden können. Es ist gut organisiert und gut benannt (um einige Funktionen zu testen, schenken Sie der Benennung mehr Aufmerksamkeit. Wenn Sie keine Tests schreiben, ist der Name für eine Einwegfunktion weniger wichtig). Es ist tendenziell parametrischer (wie Ihr Zeitbeispiel) und kann daher auch aus anderen Kontexten als dem ursprünglichen Verwendungszweck verwendet werden. Es ist TROCKEN, also weniger überladen und einfacher zu verstehen.

Ja. Es ist eine gute Praxis, testbaren Code zu schreiben, auch unabhängig vom Testen.

Carl Manaster
quelle
Ich bin nicht einverstanden damit, dass GetCurrentTime in eine Methode DRY-wrapping ist. MyGetCurrentTime wiederholt den Aufruf des Betriebssystems sehr oft, ohne dass dies von Vorteil ist, außer um die Testtools zu unterstützen. Das ist nur das einfachste von Beispielen, sie werden in der Realität viel schlimmer.
Gbjbaanb
1
"Wiederholen des Betriebssystemaufrufs ohne Nutzen" - bis Sie auf einem Server mit einer Uhr landen und mit einem aws-Server in einer anderen Zeitzone sprechen. Dadurch wird Ihr Code beschädigt, und Sie müssen dann Ihren gesamten Code und Code überprüfen Aktualisieren Sie es, um MyGetCurrentTime zu verwenden, das stattdessen UTC zurückgibt. ; Zeitversatz, Sommerzeit und andere Gründe, warum es möglicherweise keine gute Idee ist, dem Aufruf des Betriebssystems blind zu vertrauen oder zumindest einen einzigen Punkt zu haben, an dem Sie einen anderen Ersatz einsetzen können.
Andrew Hill
8

Das Schreiben von testbarem Code ist wichtig, wenn Sie nachweisen möchten , dass Ihr Code tatsächlich funktioniert.

Ich stimme der negativen Einstellung zu, Ihren Code in abscheuliche Verzerrungen zu verwandeln, nur um ihn an ein bestimmtes Test-Framework anzupassen.

Auf der anderen Seite musste sich jeder hier irgendwann mit dieser 1000 Zeilen langen magischen Funktion auseinandersetzen, die nur abscheulich zu handhaben ist und praktisch nicht berührt werden kann, ohne eine oder mehrere obskure, nicht offensichtliche Abhängigkeiten irgendwo anders (oder irgendwo in sich selbst, wo die Abhängigkeit fast unmöglich zu visualisieren ist) und per definitionem nicht testbar sind. Die Vorstellung (die nicht ohne Grund ist), dass Test-Frameworks überflüssig geworden sind, sollte meiner Meinung nach nicht als kostenlose Lizenz zum Schreiben von schlechtem, nicht testbarem Code angesehen werden.

Testgetriebene Entwicklungsideale tendieren dazu, zum Beispiel Verfahren mit einer Verantwortung zu schreiben, und das ist definitiv eine gute Sache. Persönlich würde ich sagen, dass Sie sich auf eine einzige Verantwortung, eine einzige Quelle der Wahrheit, einen kontrollierten Bereich (keine verrückten globalen Variablen) und ein Minimum an spröden Abhängigkeiten verlassen sollten. Ihr Code wird dann überprüfbar sein. Kann mit einem bestimmten Test-Framework getestet werden? Wer weiß. Aber dann ist es vielleicht das Test-Framework, das sich an guten Code anpassen muss, und nicht umgekehrt.

Aber nur um klar zu sein, Code, der so clever oder so lang und / oder voneinander abhängig ist, dass er von einem anderen Menschen nicht leicht verstanden werden kann, ist kein guter Code. Zufälligerweise ist es auch kein Code, der einfach getestet werden kann.

Ist testbarer Code, der sich meiner Zusammenfassung nähert, besserer Code?

Ich weiß es nicht, vielleicht auch nicht. Die Leute hier haben einige gültige Punkte.

Ich glaube jedoch, dass besserer Code in der Regel auch testbarer Code ist.

Und wenn Sie über seriöse Software für ernsthafte Aufgaben sprechen, ist der Versand von nicht getestetem Code nicht die verantwortungsvollste Sache, die Sie mit dem Geld Ihres Arbeitgebers oder Ihrer Kunden machen können.

Es ist auch wahr, dass ein Code strengere Tests erfordert als ein anderer Code, und es ist ein wenig albern, etwas anderes vorzutäuschen. Wie wären Sie gerne Astronaut im Space Shuttle gewesen, wenn das Menüsystem, das Sie mit den wichtigen Systemen des Shuttles verbunden hat, nicht getestet worden wäre? Oder ein Mitarbeiter eines Kernkraftwerks, bei dem die Temperaturüberwachung der Softwaresysteme im Reaktor nicht getestet wurde? Erfordert ein Teil des Codes, der einen einfachen Nur-Lese-Bericht generiert, einen Container-LKW voller Dokumentation und tausend Komponententests? Ich hoffe sicher nicht. Ich sage nur ...

Craig
quelle
1
"Besserer Code ist in der Regel auch testbarer Code" Dies ist der Schlüssel. Es testbar zu machen, macht es nicht besser. Wenn Sie es häufig verbessern, wird es testbar, und die Tests enthalten häufig Informationen, mit denen Sie es verbessern können. Das bloße Vorhandensein von Tests bedeutet jedoch keine Qualität, und es gibt (seltene) Ausnahmen.
Anaximander
1
Genau. Betrachten Sie das Gegenteil. Wenn es sich um nicht testbaren Code handelt, wird er nicht getestet. Wenn es nicht getestet wurde, woher wissen Sie, ob es funktioniert oder nicht, außer in einer Live-Situation?
pjc50
1
Alle Tests beweisen, dass der Code die Tests besteht. Andernfalls wäre der von Einheiten getestete Code fehlerfrei und wir wissen, dass dies nicht der Fall ist.
wobbily_col
@anaximander Genau. Es besteht zumindest die Möglichkeit, dass das bloße Vorhandensein von Tests eine Kontraindikation darstellt, die zu einem Code mit schlechterer Qualität führt, wenn der Fokus ausschließlich auf dem Aktivieren der Kontrollkästchen liegt. "Mindestens sieben Testeinheiten für jede Funktion?" "Prüfen." Aber ich glaube wirklich, dass es einfacher sein wird, den Code zu testen, wenn es sich um Qualitätscode handelt.
Craig
1
... aber aus Tests Bürokratie zu machen, kann eine völlige Verschwendung sein und keine nützlichen Informationen oder vertrauenswürdigen Ergebnisse hervorbringen. Ungeachtet; Ich wünschte wirklich, jemand hätte den SSL Heartbleed- Fehler getestet , ja? oder der Apple Goto Fail Bug?
Craig
5

Für mich scheint es jedoch übertrieben, die Zeit als Argument zu vergehen.

Sie haben Recht, und mit dem Verspotten können Sie den Code testbar machen und vermeiden, dass die Zeit vergeht (Wortspielabsicht undefiniert). Beispielcode:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

Angenommen, Sie möchten testen, was während einer Schaltsekunde passiert. Wie Sie sagen, um dies zu testen, müssten Sie den (Produktions-) Code ändern:

def time_of_day(now=None):
    now = now if now is not None else datetime.datetime.utcnow()
    return now.strftime('%H:%M:%S')

Wenn Python Schaltsekunden unterstützt, sieht der Testcode folgendermaßen aus:

def test_handle_leap_second(self):
    actual = time_of_day(
        now=datetime.datetime(year=2015, month=6, day=30, hour=23, minute=59, second=60)
    expected = '23:59:60'
    self.assertEquals(actual, expected)

Sie können dies testen, aber der Code ist komplexer als erforderlich, und die Tests können den Codezweig, für den der meiste Produktionscode verwendet wird (dh für den kein Wert übergeben wird ) , immer noch nicht zuverlässig ausüben now. Sie umgehen dies, indem Sie einen Schein verwenden . Ausgehend vom ursprünglichen Produktionscode:

def time_of_day():
    return datetime.datetime.utcnow().strftime('%H:%M:%S')

Testcode:

@unittest.patch('datetime.datetime.utcnow')
def test_handle_leap_second(self, utcnow_mock):
    utcnow_mock.return_value = datetime.datetime(
        year=2015, month=6, day=30, hour=23, minute=59, second=60)
    actual = time_of_day()
    expected = '23:59:60'
    self.assertEquals(actual, expected)

Dies bietet mehrere Vorteile:

  • Sie testen time_of_day unabhängig von den Abhängigkeiten.
  • Sie testen denselben Codepfad wie den Produktionscode.
  • Der Produktionscode ist so einfach wie möglich.

Nebenbei ist zu hoffen, dass zukünftige Spott-Frameworks solche Dinge einfacher machen werden. Da Sie beispielsweise die verspottete Funktion als Zeichenfolge bezeichnen müssen, können Sie nicht einfach IDEs veranlassen, diese automatisch zu ändern, wenn eine time_of_dayandere Quelle für die Zeit verwendet wird.

l0b0
quelle
Zu Ihrer Information: Ihr Standardargument ist falsch. Sie wird nur einmal definiert, sodass Ihre Funktion immer die Zeit zurückgibt, zu der sie zum ersten Mal ausgewertet wurde.
Ahruss
4

Eine Eigenschaft von gut geschriebenem Code ist, dass er robust gegen Änderungen ist . Das heißt, wenn sich Anforderungen ändern, sollte die Änderung des Codes proportional sein. Dies ist ideal (und wird nicht immer erreicht), aber das Schreiben von testbarem Code hilft uns, diesem Ziel näher zu kommen.

Warum bringt es uns näher? In der Produktion arbeitet unser Code innerhalb der Produktionsumgebung, einschließlich der Integration und Interaktion mit all unserem anderen Code. In Unit-Tests fegen wir einen Großteil dieser Umgebung weg. Unser Code ist jetzt robust gegenüber Änderungen, da Tests eine Änderung darstellen . Wir verwenden die Einheiten auf unterschiedliche Art und Weise, mit unterschiedlichen Eingaben (Mocks, schlechte Eingaben, die möglicherweise niemals tatsächlich weitergegeben werden, usw.), als wir sie in der Produktion verwenden würden.

Dies bereitet unseren Code auf den Tag vor, an dem Änderungen in unserem System stattfinden. Angenommen, unsere Zeitberechnung benötigt eine andere Zeit, die auf einer Zeitzone basiert. Jetzt können wir diese Zeit verstreichen lassen und müssen keine Änderungen am Code vornehmen. Wenn wir keine Zeit verstreichen lassen und die aktuelle Zeit verwenden möchten, können wir einfach ein Standardargument verwenden. Unser Code ist robust zu ändern, weil es testbar ist.

cbojar
quelle
4

Nach meiner Erfahrung besteht eine der wichtigsten und weitreichendsten Entscheidungen, die Sie beim Erstellen eines Programms treffen, darin, wie Sie den Code in Einheiten aufteilen (wobei "units" im weitesten Sinne verwendet wird). Wenn Sie eine klassenbasierte OO-Sprache verwenden, müssen Sie alle internen Mechanismen, die zum Implementieren des Programms verwendet werden, in eine Reihe von Klassen aufteilen. Dann müssen Sie den Code jeder Klasse in eine Reihe von Methoden aufteilen. In einigen Sprachen können Sie festlegen, wie der Code in Funktionen unterteilt werden soll. Oder wenn Sie die SOA-Sache tun, müssen Sie entscheiden, wie viele Dienste Sie erstellen und was in jedem Dienst enthalten ist.

Die von Ihnen gewählte Aufteilung hat enorme Auswirkungen auf den gesamten Prozess. Eine gute Auswahl erleichtert das Schreiben des Codes und führt zu weniger Fehlern (noch bevor Sie mit dem Testen und Debuggen beginnen). Sie erleichtern das Ändern und Warten. Interessanterweise stellt sich heraus, dass eine gute Panne in der Regel auch einfacher zu testen ist als eine schlechte.

Warum ist das so? Ich glaube nicht, dass ich alle Gründe verstehen und erklären kann. Ein Grund dafür ist jedoch, dass eine gute Aufschlüsselung ausnahmslos die Auswahl einer moderaten "Korngröße" für Implementierungseinheiten bedeutet. Sie möchten nicht zu viel Funktionalität und Logik in eine einzelne Klasse / Methode / Funktion / Modul / etc. Dies erleichtert das Lesen und Schreiben Ihres Codes, erleichtert aber auch das Testen.

Es ist jedoch nicht nur das. Ein gutes internes Design bedeutet, dass das erwartete Verhalten (Ein- / Ausgänge / usw.) jeder Implementierungseinheit klar und präzise definiert werden kann. Dies ist wichtig für das Testen. Ein gutes Design bedeutet normalerweise, dass jede Implementierungseinheit eine moderate Anzahl von Abhängigkeiten von den anderen aufweist. Das erleichtert das Lesen und Verstehen Ihres Codes, erleichtert aber auch das Testen. Die Gründe gehen weiter; Vielleicht können andere mehr Gründe ausdrücken, die ich nicht kann.

In Bezug auf das Beispiel in Ihrer Frage ist "gutes Code-Design" meiner Meinung nach nicht gleichbedeutend damit, dass alle externen Abhängigkeiten (z. B. eine Abhängigkeit von der Systemuhr) immer "injiziert" werden sollten. Das mag eine gute Idee sein, aber es ist ein anderes Thema als das, was ich hier beschreibe, und ich werde nicht auf die Vor- und Nachteile eingehen.

Im Übrigen bedeutet dies nicht, dass Sie Ihren Code nicht isoliert testen können, selbst wenn Sie Systemfunktionen direkt aufrufen, die die aktuelle Uhrzeit zurückgeben, auf das Dateisystem zugreifen und so weiter. Der Trick besteht darin, eine spezielle Version der Standardbibliotheken zu verwenden, mit der Sie die Rückgabewerte von Systemfunktionen fälschen können. Ich habe noch nie gesehen, dass andere diese Technik erwähnen, aber es ist ziemlich einfach, mit vielen Sprachen und Entwicklungsplattformen umzugehen. (Hoffentlich ist Ihre Sprachlaufzeit Open Source und einfach zu erstellen. Wenn die Ausführung Ihres Codes einen Verknüpfungsschritt umfasst, ist es hoffentlich auch einfach zu steuern, mit welchen Bibliotheken er verknüpft ist.)

Zusammenfassend ist testbarer Code nicht unbedingt "guter" Code, aber "guter" Code ist normalerweise testbar.

Alex D
quelle
1

Wenn Sie mit SOLID- Prinzipien arbeiten, sind Sie auf der guten Seite, insbesondere wenn Sie diese mit KISS , DRY und YAGNI erweitern .

Ein für mich fehlender Punkt ist die Komplexität einer Methode. Ist es eine einfache Getter / Setter-Methode? Dann wäre es Zeitverschwendung, Tests zu schreiben, um Ihr Test-Framework zu erfüllen.

Wenn es sich um eine komplexere Methode handelt, bei der Sie Daten manipulieren und sicherstellen möchten, dass sie auch dann funktionieren, wenn Sie die interne Logik ändern müssen, ist dies ein guter Aufruf für eine Testmethode. Oft musste ich nach einigen Tagen / Wochen / Monaten einen Code ändern, und ich war wirklich froh, den Testfall zu haben. Als ich die Methode zum ersten Mal entwickelte, testete ich sie mit der Testmethode und war mir sicher, dass sie funktionieren wird. Nach der Änderung funktionierte mein primärer Testcode immer noch. Ich war mir also sicher, dass meine Änderung keinen alten Code in der Produktion beschädigte.

Ein weiterer Aspekt beim Schreiben von Tests besteht darin, anderen Entwicklern den Umgang mit Ihrer Methode zu zeigen. Oft sucht ein Entwickler nach einem Beispiel für die Verwendung einer Methode und den Rückgabewert.

Nur meine zwei Cent .

BtD
quelle