Heute hatte ich ein interessantes Gespräch mit einem Kollegen.
Ich bin ein defensiver Programmierer. Ich glaube, dass die Regel " eine Klasse muss sicherstellen, dass ihre Objekte einen gültigen Zustand haben, wenn mit von außerhalb der Klasse interagieren " immer eingehalten werden muss. Der Grund für diese Regel ist, dass die Klasse nicht weiß, wer ihre Benutzer sind, und dass sie vorhersehbar scheitern sollte, wenn auf illegale Weise mit ihr interagiert wird. Meiner Meinung nach gilt diese Regel für alle Klassen.
In der speziellen Situation, in der ich heute eine Diskussion hatte, habe ich Code geschrieben, der bestätigt, dass die Argumente für meinen Konstruktor korrekt sind (z. B. muss ein Integer-Parameter> 0 sein), und wenn die Vorbedingung nicht erfüllt ist, wird eine Ausnahme ausgelöst. Mein Kollege hingegen ist der Meinung, dass eine solche Überprüfung überflüssig ist, da Unit-Tests alle falschen Verwendungen der Klasse aufdecken sollten. Darüber hinaus ist er der Ansicht, dass die Validierung defensiver Programme auch auf Einheit getestet werden sollte, sodass defensives Programmieren viel Arbeit kostet und daher für TDD nicht optimal ist.
Stimmt es, dass TDD defensive Programme ersetzen kann? Ist eine Parameterüberprüfung (und damit meine ich keine Benutzereingabe) in der Folge unnötig? Oder ergänzen sich die beiden Techniken?
quelle
Antworten:
Das ist lächerlich. TDD zwingt den Code, Tests zu bestehen, und zwingt den gesamten Code, einige Tests durchzuführen. Es verhindert nicht, dass Ihre Kunden Code falsch aufrufen, und es verhindert auf magische Weise, dass Programmierer Testfälle verpassen.
Keine Methode kann Benutzer zur korrekten Verwendung von Code zwingen.
Es gibt ein kleines Argument dafür, dass Sie, wenn Sie TDD perfekt durchgeführt hätten, Ihren> 0-Check in einem Testfall abgefangen hätten, bevor Sie ihn implementiert haben, und dies behoben haben - wahrscheinlich indem Sie den Check hinzugefügt haben. Wenn Sie jedoch TDD verwenden, wird Ihre Anforderung (> 0 im Konstruktor) zuerst als fehlgeschlagener Testfall angezeigt. So geben Sie den Test, nachdem Sie Ihren Scheck hinzufügen.
Es ist auch sinnvoll, einige der Defensivbedingungen zu testen (Sie haben die Logik hinzugefügt, warum möchten Sie nicht etwas testen, das so einfach zu testen ist?). Ich bin mir nicht sicher, warum Sie damit nicht einverstanden zu sein scheinen.
TDD wird die Tests entwickeln. Durch die Implementierung der Parameterüberprüfung werden sie bestanden.
quelle
Defensives Programmieren und Unit-Tests sind zwei verschiedene Methoden, um Fehler zu erkennen, und haben jeweils unterschiedliche Stärken. Wenn Sie nur eine Methode zum Erkennen von Fehlern verwenden, werden Ihre Fehlererkennungsmechanismen zerbrechlich. Wenn Sie beides verwenden, werden Fehler abgefangen, die möglicherweise von der einen oder anderen Seite übersehen wurden, auch in Code, der keine öffentlich zugängliche API ist. Möglicherweise hat jemand vergessen, einen Komponententest für ungültige Daten hinzuzufügen, die an die öffentliche API übergeben wurden. Wenn Sie alles an geeigneten Stellen überprüfen, besteht eine größere Wahrscheinlichkeit, dass der Fehler behoben wird.
In der Informationssicherheit wird dies als Tiefenverteidigung bezeichnet. Mit mehreren Verteidigungsebenen ist sichergestellt, dass bei einem Ausfall noch weitere vorhanden sind.
Ihr Kollege hat in einer Hinsicht Recht: Sie sollten Ihre Validierungen testen, aber dies ist keine "unnötige Arbeit". Es ist dasselbe wie das Testen eines anderen Codes. Sie möchten sicherstellen, dass alle Verwendungen, auch ungültige, ein erwartetes Ergebnis haben.
quelle
TDD ersetzt keinesfalls die defensive Programmierung. Stattdessen können Sie TDD verwenden, um sicherzustellen, dass alle Abwehrmechanismen vorhanden sind und wie erwartet funktionieren.
In TDD sollten Sie keinen Code schreiben, ohne vorher einen Test zu schreiben - folgen Sie dem Rot-Grün-Refaktor-Zyklus religiös. Das heißt, wenn Sie eine Validierung hinzufügen möchten, schreiben Sie zuerst einen Test, der diese Validierung erfordert. Rufen Sie die betreffende Methode mit negativen Zahlen und mit Null auf und erwarten Sie, dass sie eine Ausnahme auslöst.
Vergessen Sie auch nicht den "Refactor" -Schritt. Während TDD testgetrieben ist , bedeutet dies nicht nur testgetrieben . Sie sollten trotzdem das richtige Design anwenden und vernünftigen Code schreiben. Das Schreiben von defensivem Code ist sinnvoll, da dadurch die Erwartungen expliziter und der Code insgesamt robuster werden. Das frühzeitige Erkennen möglicher Fehler erleichtert das Debuggen.
Aber sollen wir nicht Tests verwenden, um Fehler zu lokalisieren? Behauptungen und Tests ergänzen sich. Eine gute Teststrategie wird verschiedene Ansätze mischen , um sicherzustellen, dass die Software robust ist. Nur Unit-Tests oder nur Integrationstests oder nur Aussagen im Code sind unbefriedigend. Sie benötigen eine gute Kombination, um mit akzeptablem Aufwand ein ausreichendes Maß an Vertrauen in Ihre Software zu erreichen.
Dann gibt es ein sehr großes begriffliches Missverständnis Ihres Mitarbeiters: Unit-Tests können niemals Verwendungen Ihrer Klasse testen , nur dass die Klasse selbst wie erwartet isoliert arbeitet. Sie würden Integrationstests verwenden, um zu überprüfen, ob die Interaktion zwischen verschiedenen Komponenten funktioniert, aber die kombinatorische Explosion möglicher Testfälle macht es unmöglich, alles zu testen. Integrationstests sollten sich daher auf einige wichtige Fälle beschränken. Detailliertere Tests, die auch Rand- und Fehlerfälle abdecken, eignen sich besser für Komponententests.
quelle
Tests sollen die defensive Programmierung unterstützen und sicherstellen
Defensive Programmierung schützt die Integrität des Systems zur Laufzeit.
Tests sind (meist statische) Diagnosewerkzeuge. Zur Laufzeit sind Ihre Tests nicht in Sicht. Sie sind wie ein Gerüst, mit dem eine hohe Mauer oder eine Felskuppel errichtet wurde. Sie lassen keine wichtigen Teile aus der Struktur heraus, weil Sie ein Baugerüst haben, das es während des Aufbaus hält. Sie haben ein Gerüst, das es während des Aufbaus hochhält , um das Einsetzen aller wichtigen Teile zu erleichtern .
EDIT: Eine Analogie
Was ist mit einer Analogie zu Kommentaren im Code?
Kommentare haben ihren Zweck, können aber überflüssig oder sogar schädlich sein. Wenn Sie beispielsweise den Kommentaren grundlegende Kenntnisse über den Code hinzufügen und dann den Code ändern, werden die Kommentare im besten Fall irrelevant und im schlimmsten Fall schädlich.
Nehmen wir also an, Sie stecken viel Grundwissen über Ihre Codebasis in die Tests, z. B. kann MethodA keine Null annehmen, und das Argument von MethodB muss es sein
> 0
. Dann ändert sich der Code. Null ist für A jetzt in Ordnung, und B kann Werte von -10 annehmen. Die vorhandenen Tests sind jetzt funktional falsch, werden aber weiterhin bestanden.Ja, Sie sollten die Tests zur gleichen Zeit aktualisieren, zu der Sie den Code aktualisieren. Sie sollten auch Kommentare aktualisieren (oder entfernen), während Sie den Code aktualisieren. Aber wir alle wissen, dass diese Dinge nicht immer passieren und dass Fehler gemacht werden.
Die Tests verifizieren das Verhalten des Systems. Dieses tatsächliche Verhalten ist systemimmanent und nicht testimmanent.
Was könnte möglicherweise falsch laufen?
Das Ziel in Bezug auf die Tests ist es, sich alles auszudenken, was schief gehen könnte, einen Test dafür zu schreiben, der das richtige Verhalten überprüft, und dann den Laufzeitcode so zu erstellen, dass er alle Tests besteht.
Was bedeutet, dass defensive Programmierung der Punkt ist .
TDD treibt defensive Programmierung an, wenn die Tests umfassend sind.
Mehr Tests, mehr defensive Programmierung
Wenn unvermeidlich Fehler gefunden werden, werden weitere Tests geschrieben, um die Bedingungen zu modellieren, unter denen der Fehler auftritt. Anschließend wird der Code mit dem Code zum Bestehen dieser Tests festgelegt, und die neuen Tests verbleiben in der Testsuite.
Eine gute Reihe von Tests wird sowohl gute als auch schlechte Argumente an eine Funktion / Methode übergeben und konsistente Ergebnisse erwarten. Dies bedeutet wiederum, dass die getestete Komponente Voraussetzungsprüfungen (defensive Programmierung) verwendet, um die ihr übergebenen Argumente zu bestätigen.
Im Allgemeinen ...
Wenn beispielsweise ein Nullargument für eine bestimmte Prozedur ungültig ist, besteht mindestens ein Test die Null und es wird eine Ausnahme / ein Fehler "ungültiges Nullargument" erwartet.
Mindestens ein anderer Test wird natürlich ein gültiges Argument übergeben - oder ein großes Array durchlaufen und unzählige gültige Argumente übergeben - und bestätigen, dass der resultierende Status angemessen ist.
Wenn ein Test nicht der Fall ist , dass die Null - Argument übergeben und mit der erwarteten Ausnahme schlug bekommen (und diese Ausnahme geworfen wurde , weil der Code defensiv den Zustand an sie übergeben markiert) ist , dann kann der null bis zu einer Eigenschaft einer Klasse zugeordnet beenden oder begraben in einer Art Sammlung, in der es nicht sein sollte.
Dies kann zu unerwartetem Verhalten in einem ganz anderen Teil des Systems führen, an den die Klasseninstanz übergeben wird, und zwar in einem entfernten geografischen Gebietsschema, nachdem die Software ausgeliefert wurde . Und das ist die Art von Dingen, die wir eigentlich vermeiden wollen, oder?
Es könnte noch schlimmer sein. Die Klasseninstanz mit dem ungültigen Status konnte serialisiert und gespeichert werden, um nur dann einen Fehler zu verursachen, wenn sie für eine spätere Verwendung wiederhergestellt wird. Meine Güte, ich weiß nicht, vielleicht handelt es sich um eine Art mechanisches Steuersystem, das nach einem Herunterfahren nicht neu gestartet werden kann, weil es seinen eigenen dauerhaften Konfigurationsstatus nicht deserialisieren kann. Oder die Klasseninstanz könnte serialisiert und an ein völlig anderes System übergeben werden, das von einer anderen Entität erstellt wurde, und dieses System könnte abstürzen.
Vor allem, wenn die Programmierer dieses anderen Systems nicht defensiv codierten.
quelle
Anstelle von TDD sprechen wir über "Softwaretests" im Allgemeinen und von "defensiver Programmierung" im Allgemeinen. Lassen Sie uns über meine Lieblingsmethode für defensives Programmieren sprechen, die darin besteht, Behauptungen zu verwenden.
Da wir also Softwaretests durchführen, sollten wir das Platzieren von Assert-Anweisungen im Produktionscode aufgeben, richtig? Lassen Sie mich die Art und Weise zählen, in der dies falsch ist:
Zusicherungen sind optional. Wenn Sie sie also nicht mögen, führen Sie einfach Ihr System mit deaktivierten Zusicherungen aus.
Assertions prüfen Dinge, die beim Testen nicht möglich sind (und sollten), da das Testen eine Black-Box-Ansicht Ihres Systems haben soll, während Assertions eine White-Box-Ansicht haben sollen. (Natürlich, da sie darin leben.)
Behauptungen sind ein hervorragendes Dokumentationswerkzeug. Kein Kommentar war oder wird jemals so eindeutig sein wie ein Code, der dasselbe behauptet. Außerdem ist die Dokumentation mit der Entwicklung des Codes in der Regel veraltet und für den Compiler in keiner Weise durchsetzbar.
Behauptungen können Fehler im Testcode auffangen. Haben Sie jemals eine Situation erlebt, in der ein Test fehlschlägt und Sie nicht wissen, wer falsch liegt - der Produktionscode oder der Test?
Behauptungen können sachdienlicher sein als das Testen. Bei den Tests wird überprüft, was durch die funktionalen Anforderungen vorgeschrieben ist. Der Code muss jedoch häufig bestimmte Annahmen treffen, die weitaus technischer sind. Leute, die funktionale Anforderungsdokumente schreiben, denken selten an eine Division durch Null.
Behauptungen weisen auf Fehler hin, auf die das Testen nur allgemein hinweist. Ihr Test stellt also einige umfangreiche Voraussetzungen auf, ruft einen längeren Code auf, sammelt die Ergebnisse und stellt fest, dass sie nicht den Erwartungen entsprechen. Bei einer ausreichenden Fehlerbehebung werden Sie schließlich genau feststellen, wo Fehler aufgetreten sind, aber Behauptungen werden diese normalerweise zuerst finden.
Behauptungen reduzieren die Programmkomplexität. Jede einzelne Codezeile, die Sie schreiben, erhöht die Programmkomplexität. Behauptungen und das Schlüsselwort
final
(readonly
) sind die einzigen zwei mir bekannten Konstrukte, die die Programmkomplexität reduzieren. Das ist unbezahlbar.Behauptungen helfen dem Compiler, Ihren Code besser zu verstehen. Versuchen Sie dies bitte zu Hause:
void foo( Object x ) { assert x != null; if( x == null ) { } }
Ihr Compiler sollte eine Warnung ausgeben, die Sie darauf hinweist, dass die Bedingungx == null
immer falsch ist. Das kann sehr nützlich sein.Das obige war eine Zusammenfassung eines Beitrags aus meinem Blog, 21.09.2014 "Behauptungen und Tests"
quelle
Ich glaube, den meisten Antworten fehlt eine kritische Unterscheidung: Es hängt davon ab, wie Ihr Code verwendet wird.
Wird das betreffende Modul von anderen Clients unabhängig von der zu testenden Anwendung verwendet? Wenn Sie eine Bibliothek oder API zur Verwendung durch Dritte bereitstellen, können Sie nicht sicherstellen, dass sie Ihren Code nur mit einer gültigen Eingabe aufrufen. Sie müssen alle Eingaben validieren.
Wenn das betreffende Modul jedoch nur von dem Code verwendet wird, den Sie steuern, hat Ihr Freund möglicherweise einen Punkt. Sie können Unit-Tests verwenden, um zu überprüfen, ob das betreffende Modul nur mit einer gültigen Eingabe aufgerufen wird. Voraussetzungsprüfungen könnten immer noch als gute Praxis angesehen werden, aber es ist ein Kompromiss: Wenn Sie den Code verunreinigen, der nach Bedingungen sucht, von denen Sie wissen , dass sie niemals auftreten können, wird die Absicht des Codes nur verschleiert.
Ich bin nicht einverstanden, dass Vorbedingungsprüfungen mehr Komponententests erfordern. Wenn Sie entscheiden, dass Sie einige Formen ungültiger Eingaben nicht testen müssen, sollte es keine Rolle spielen, ob die Funktion Voraussetzungsprüfungen enthält oder nicht. Denken Sie daran, dass Tests das Verhalten und nicht die Implementierungsdetails überprüfen sollten.
quelle
Dieses Argument verwirrt mich, denn als ich anfing, TDD zu üben, reagierten meine Komponententests der Form "Objekt reagiert <auf bestimmte Weise>, wenn <ungültige Eingabe>" zwei- oder dreimal zunahm. Ich frage mich, wie es Ihrem Kollegen gelingt, diese Art von Komponententests erfolgreich zu bestehen, ohne dass seine Funktionen validiert werden.
Der umgekehrte Fall, dass Unit-Tests zeigen, dass Sie niemals schlechte Ausgaben produzieren , die an die Argumente anderer Funktionen weitergegeben werden, ist viel schwieriger zu beweisen. Wie im ersten Fall hängt es stark von der sorgfältigen Erfassung von Randfällen ab, aber Sie müssen zusätzlich sicherstellen, dass alle Funktionseingaben von den Ausgängen anderer Funktionen stammen, deren Ausgänge Sie in der Einheit getestet haben, und nicht etwa von Benutzereingaben oder Module von Drittanbietern.
Mit anderen Worten, das, was TDD tut, hindert Sie nicht daran , Validierungscode zu benötigen, und hilft Ihnen auch nicht, ihn zu vergessen .
quelle
Ich denke, ich interpretiere die Bemerkungen Ihres Kollegen anders als die meisten anderen Antworten.
Mir scheint das Argument zu sein:
Für mich hat dieses Argument eine gewisse Logik, setzt aber zu viel Vertrauen in Unit-Tests, um jede mögliche Situation abzudecken. Die einfache Tatsache ist, dass 100% Leitungs- / Verzweigungs- / Pfadabdeckung nicht notwendigerweise jeden Wert ausübt , den der Anrufer möglicherweise übergibt, wohingegen 100% aller möglichen Zustände des Anrufers (dh aller möglichen Werte seiner Eingaben) abgedeckt sind und Variablen) ist rechnerisch nicht realisierbar.
Aus diesem Grund würde ich es vorziehen, die Aufrufer in einem Komponententest zu testen, um sicherzustellen, dass sie (soweit die Tests durchgeführt werden) niemals fehlerhafte Werte übergeben. Außerdem würde ich verlangen, dass Ihre Komponente auf erkennbare Weise ausfällt, wenn ein fehlerhafter Wert übergeben wird ( zumindest soweit es möglich ist, schlechte Werte in der Sprache Ihrer Wahl zu erkennen). Dies hilft beim Debuggen, wenn Probleme bei Integrationstests auftreten, und hilft auch allen Benutzern Ihrer Klasse, die ihre Codeeinheit nicht unbedingt von dieser Abhängigkeit isolieren.
Beachten Sie jedoch, dass, wenn Sie das Verhalten Ihrer Funktion dokumentieren und testen, wenn ein Wert <= 0 übergeben wird, negative Werte nicht mehr ungültig sind (zumindest nicht mehr ungültig als ein Argument dafür
throw
ist) Auch ist dokumentiert, um eine Ausnahme auszulösen!). Anrufer sind berechtigt, sich auf dieses Abwehrverhalten zu verlassen. Wenn die Sprache es zulässt, kann es sein, dass dies auf jeden Fall das beste Szenario ist - die Funktion hat keine "ungültigen Eingaben", aber Anrufer, die nicht erwarten, die Funktion zum Auslösen einer Ausnahme zu provozieren, sollten ausreichend Unit-getestet werden, um sicherzustellen, dass sie keine " Übergeben Sie keine Werte, die dies verursachen.Obwohl ich denke, dass Ihr Kollege etwas weniger falsch ist als die meisten Antworten, komme ich zu dem gleichen Schluss, dass sich die beiden Techniken ergänzen. Programmieren Sie defensiv, dokumentieren Sie Ihre Defensiv-Checks und testen Sie sie. Die Arbeit ist nur "unnötig", wenn Benutzer Ihres Codes nicht von nützlichen Fehlermeldungen profitieren können, wenn sie Fehler machen. Theoretisch werden sie die Fehlermeldungen nie sehen, wenn sie ihren gesamten Code gründlich Unit-testen, bevor sie ihn in Ihren integrieren, und wenn in ihren Tests niemals Fehler auftreten. Selbst wenn sie TDD und Total Dependency Injection durchführen, können sie dies in der Praxis noch während der Entwicklung untersuchen oder es kann zu einem Zeitversatz bei ihren Tests kommen. Das Ergebnis ist, dass sie Ihren Code aufrufen, bevor ihr Code perfekt ist!
quelle
Öffentliche Schnittstellen können und werden missbraucht
Die Behauptung Ihres Mitarbeiters "Komponententests sollten falsche Verwendungen der Klasse aufdecken" ist für jede Schnittstelle, die nicht privat ist, streng falsch. Wenn eine öffentliche Funktion kann mit ganzzahligen Argumenten aufgerufen wird, dann kann und wird mit heißen beliebigen Integer - Argumenten, und der Code sollte entsprechend verhalten. Wenn eine öffentliche Funktionssignatur z. B. den Java Double-Typ akzeptiert, sind Null, NaN, MAX_VALUE, -Inf alle möglichen Werte. Ihre Komponententests können keine falschen Verwendungen der Klasse feststellen, da diese Tests den Code, der diese Klasse verwendet, nicht testen können, da dieser Code noch nicht geschrieben ist, möglicherweise nicht von Ihnen geschrieben wurde und definitiv außerhalb des Bereichs Ihrer Komponententests liegt .
Auf der anderen Seite kann dieser Ansatz für die (hoffentlich viel zahlreicheren) privaten Eigenschaften gelten - wenn eine Klasse sicherstellen kann , dass eine Tatsache immer zutrifft (z. B. Eigenschaft X kann niemals null sein, die Ganzzahlposition überschreitet nicht die maximale Länge Wenn die Funktion A aufgerufen wird, sind alle vorausgesetzten Datenstrukturen gut ausgebildet. Es kann angebracht sein, dies aus Leistungsgründen nicht immer wieder zu überprüfen, sondern sich stattdessen auf Komponententests zu verlassen.
quelle
Die Abwehr von Missbrauch ist ein Feature , das aufgrund einer Anforderung entwickelt wurde. (Nicht alle Schnittstellen erfordern strenge Kontrollen gegen Missbrauch, zum Beispiel sehr eng verwendete interne.)
Die Funktion muss getestet werden: Funktioniert die Abwehr von Missbrauch tatsächlich? Das Ziel des Testens dieser Funktion besteht darin, zu zeigen, dass dies nicht der Fall ist: Missbrauch des Moduls, der nicht von seinen Überprüfungen erfasst wird.
Wenn bestimmte Überprüfungen erforderlich sind, ist es in der Tat unsinnig zu behaupten, dass das Vorhandensein einiger Tests sie unnötig macht. Wenn es sich um eine Funktion handelt, die (beispielsweise) eine Ausnahme auslöst, wenn Parameter drei negativ ist, ist dies nicht verhandelbar. das soll es tun.
Ich vermute jedoch, dass Ihr Kollege aus der Sicht einer Situation, in der keine spezifischen Kontrollen der Eingaben erforderlich sind , mit spezifischen Reaktionen auf schlechte Eingaben tatsächlich Sinn macht Robustheit.
Überprüfungen beim Eintritt in eine Funktion der obersten Ebene dienen zum Teil dazu, einen schwachen oder schlecht getesteten internen Code vor unerwarteten Kombinationen von Parametern zu schützen (wenn der Code gut getestet ist, sind die Überprüfungen nicht erforderlich: Der Code kann nur Wetter "die schlechten Parameter).
Die Idee des Kollegen ist wahr, und was er wahrscheinlich meint, ist folgende: Wenn wir eine Funktion aus sehr robusten Teilen niedrigerer Ebenen aufbauen, die defensiv codiert und einzeln gegen jeglichen Missbrauch getestet werden, ist es möglich, dass die Funktion höherer Ebenen eine Funktion ist robust ohne eigene umfangreiche Selbstkontrolle.
Wenn gegen seinen Vertrag verstoßen wird, führt dies zu einem Missbrauch der Funktionen auf niedrigerer Ebene, beispielsweise durch das Auslösen von Ausnahmen oder was auch immer.
Das einzige Problem dabei ist, dass die Ausnahmen der niedrigeren Ebene nicht spezifisch für die Schnittstelle der höheren Ebene sind. Ob dies ein Problem ist, hängt von den Anforderungen ab. Wenn die Anforderung einfach lautet: "Die Funktion muss robust gegen Missbrauch sein und keine Ausnahmebedingungen auslösen, statt abstürzen zu müssen, oder weiterhin mit Abfalldaten rechnen", kann dies tatsächlich durch die Robustheit der Teile der unteren Ebene abgedeckt werden, auf denen sie sich befindet gebaut.
Wenn die Funktion eine sehr spezifische, detaillierte Fehlerberichterstattung in Bezug auf ihre Parameter erfordert, erfüllen die Prüfungen der unteren Ebene diese Anforderungen nicht vollständig. Sie sorgen nur dafür, dass die Funktion irgendwie in die Luft geht (setzt sich nicht mit einer schlechten Kombination von Parametern fort, was zu einem Müllergebnis führt). Wenn der Clientcode so geschrieben ist, dass er bestimmte Fehler abfängt und verarbeitet, funktioniert er möglicherweise nicht richtig. Der Client-Code kann selbst als Eingabe die Daten abrufen, auf denen die Parameter basieren, und von der Funktion erwarten, dass sie diese prüft und fehlerhafte Werte in die dokumentierten Fehler umsetzt (damit sie diese verarbeiten können) Fehler richtig) anstatt einige andere Fehler, die nicht behandelt werden und möglicherweise das Software-Image stoppen.
TL; DR: Ihr Kollege ist wahrscheinlich kein Idiot; Sie reden nur mit unterschiedlichen Perspektiven aneinander vorbei, weil die Anforderungen nicht vollständig festgelegt sind und jeder von Ihnen eine andere Vorstellung davon hat, was die "ungeschriebenen Anforderungen" sind. Sie denken, wenn es keine spezifischen Anforderungen an die Parameterprüfung gibt, sollten Sie die detaillierte Prüfung trotzdem verschlüsseln. der kollege denkt, lass einfach den robusten untergeordneten code explodieren, wenn die parameter falsch sind. Es ist etwas unproduktiv, über ungeschriebene Anforderungen im Code zu streiten: Sie stimmen nicht mit Anforderungen überein, sondern mit Code. Ihre Art der Codierung entspricht Ihrer Meinung nach den Anforderungen. der weg des kollegen repräsentiert seine sicht auf die anforderungen. Wenn Sie das so sehen, ist es klar, dass das, was richtig oder falsch ist, nicht stimmt. t im Code selbst; Der Code ist lediglich ein Proxy für Ihre Meinung zu den Spezifikationen.
quelle
Tests definieren den Vertrag Ihrer Klasse.
Folglich definiert das Fehlen eines Tests einen Vertrag, der undefiniertes Verhalten enthält . Also , wenn Sie passieren zu , und ungezählte Laufzeit Chaos folgt, sind Sie noch im Auftrag der Klasse.
null
Foo::Frobnicate(Widget widget)
Später entscheiden Sie, "wir wollen nicht die Möglichkeit eines undefinierten Verhaltens", was eine vernünftige Wahl ist. Das bedeutet , dass Sie ein erwartetes Verhalten für das Bestehen haben müssen ,
null
zuFoo::Frobnicate(Widget widget)
.Und Sie dokumentieren diese Entscheidung, indem Sie a
quelle
Eine gute Reihe von Tests überprüft die externe Schnittstelle Ihrer Klasse und stellt sicher, dass solche Missbräuche die richtige Reaktion hervorrufen (eine Ausnahme oder was auch immer Sie als "korrekt" definieren). Tatsächlich besteht der erste Testfall, den ich für eine Klasse schreibe, darin, ihren Konstruktor mit außerhalb des Bereichs liegenden Argumenten aufzurufen.
Die Art der defensiven Programmierung, die durch einen vollständig auf Unit-Tests basierenden Ansatz tendenziell beseitigt wird, ist die unnötige Validierung interner Invarianten, die durch externen Code nicht verletzt werden können.
Eine nützliche Idee, die ich manchmal verwende, besteht darin, eine Methode bereitzustellen, die die Invarianten des Objekts testet. Ihre Auflösungsmethode kann es aufrufen, um zu überprüfen, ob Ihre externen Aktionen für das Objekt niemals die Invarianten brechen.
quelle
Die Tests von TDD werden Fehler bei der Entwicklung des Codes auffangen .
Die Grenzen, die Sie im Rahmen der defensiven Programmierung überprüfen, werden bei der Verwendung des Codes auf Fehler stoßen .
Wenn die beiden Domänen identisch sind, das heißt, der von Ihnen geschriebene Code wird immer nur intern von diesem bestimmten Projekt verwendet, kann es sein, dass TDD die Notwendigkeit der Überprüfung der von Ihnen beschriebenen defensiven Programmierungsgrenzen ausschließt, jedoch nur, wenn diese Typen vorhanden sind Die Prüfung der Grenzen wird speziell in TDD-Tests durchgeführt .
Nehmen wir als spezifisches Beispiel an, dass eine Bibliothek mit Finanzcode unter Verwendung von TDD entwickelt wurde. Einer der Tests könnte behaupten, dass ein bestimmter Wert niemals negativ sein kann. Dadurch wird sichergestellt, dass die Entwickler der Bibliothek die Klassen bei der Implementierung der Funktionen nicht versehentlich missbrauchen.
Aber nachdem die Bibliothek freigegeben wurde und ich sie in meinem eigenen Programm verwende, hindern mich diese TDD-Tests nicht daran, einen negativen Wert zuzuweisen (vorausgesetzt, es ist verfügbar). Bounds Checking würde.
Mein Punkt ist, dass, während eine TDD-Behauptung das Problem mit dem negativen Wert angehen könnte, wenn der Code immer nur intern als Teil der Entwicklung einer größeren Anwendung (unter TDD) verwendet wird, wenn es eine Bibliothek sein wird, die von anderen Programmierern ohne TDD verwendet wird Rahmen und Tests , Grenzen prüfen Angelegenheiten.
quelle
TDD und defensive Programmierung gehen Hand in Hand. Beides zu verwenden ist nicht überflüssig, sondern komplementär. Wenn Sie eine Funktion haben, möchten Sie sicherstellen, dass die Funktion wie beschrieben funktioniert, und Tests dafür schreiben. Wenn Sie nicht abdecken, was passiert, wenn eine Eingabe, eine Rückgabe, ein Zustand usw. nicht korrekt ist, schreiben Sie Ihre Tests nicht zuverlässig genug, und Ihr Code ist auch dann fragil, wenn alle Ihre Tests bestanden wurden.
Als Embedded Engineer möchte ich am Beispiel des Schreibens einer Funktion einfach zwei Bytes addieren und das Ergebnis folgendermaßen zurückgeben:
Wenn Sie es einfach
*(sum) = a + b
machen würden, würde es funktionieren, aber nur mit einigen Eingaben.a = 1
undb = 2
würde machensum = 3
; jedoch , weil die Größe der Summe ist ein Byte,a = 100
undb = 200
würdesum = 44
aufgrund überlaufen. In C würden Sie in diesem Fall einen Fehler zurückgeben, um anzuzeigen, dass die Funktion fehlgeschlagen ist. Das Auslösen einer Ausnahme ist dasselbe in Ihrem Code. Wenn die Fehler nicht berücksichtigt werden oder getestet wird, wie sie behandelt werden sollen, funktioniert dies auf lange Sicht nicht, da sie unter diesen Umständen nicht behandelt werden und eine beliebige Anzahl von Problemen verursachen können.quelle
sum
ein Nullzeiger ist?).