it-swarm.com.de

Macht TDD die defensive Programmierung überflüssig?

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 sie von außerhalb der Klasse interagiert wird" immer eingehalten werden muss. Der Grund für diese Regel ist, dass die Klasse nicht weiß, wer ihre Benutzer sind, und dass sie vorhersehbar fehlschlagen sollte, wenn sie auf illegale Weise interagiert. 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 ganzzahliger Parameter> 0 sein). Wenn die Vorbedingung nicht erfüllt ist, wird eine Ausnahme ausgelöst. Mein Kollege hingegen ist der Ansicht, dass eine solche Prüfung überflüssig ist, da Unit-Tests falsche Verwendungen der Klasse aufdecken sollten. Darüber hinaus ist er der Ansicht, dass Defensivprogrammierungsvalidierungen auch Unit-getestet werden sollten, sodass Defensivprogrammierung viel Arbeit bedeutet und daher für TDD nicht optimal ist.

Stimmt es, dass TDD die defensive Programmierung ersetzen kann? Ist eine Parameterüberprüfung (und damit meine ich keine Benutzereingabe) nicht erforderlich? Oder ergänzen sich die beiden Techniken?

105
user2180613

Das ist lächerlich. TDD erzwingt, dass Code Tests besteht, und erzwingt, dass der gesamte Code einige Tests enthält. Es verhindert weder, dass Ihre Kunden Code falsch aufrufen, noch verhindert es auf magische Weise, dass Programmierer Testfälle verpassen.

Keine Methode kann Benutzer dazu zwingen, Code korrekt zu verwenden.

Es ist is ein kleines Argument, dass Sie, wenn Sie TDD perfekt durchgeführt hätten, Ihren> 0-Check in einem Testfall vor der Implementierung abgefangen und dies behoben hätten - wahrscheinlich, indem Sie den Check hinzugefügt haben . Wenn Sie jedoch TDD ausführen, wird Ihre Anforderung (> 0 im Konstruktor) zuerst als Testfall angezeigt, der fehlschlägt. So erhalten Sie den Test, nachdem Sie Ihren Scheck hinzugefügt haben.

Es ist auch sinnvoll, einige der Verteidigungsbedingungen zu testen (Sie haben 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.

Oder ergänzen sich die beiden Techniken?

TDD wird die Tests entwickeln. Durch die Implementierung der Parameterüberprüfung werden sie bestanden.

196
enderland

Defensive Programmierung und Unit-Tests sind zwei verschiedene Methoden, um Fehler zu erkennen, und haben jeweils unterschiedliche Stärken. Wenn Sie nur eine Methode zur Fehlererkennung verwenden, sind Ihre Fehlererkennungsmechanismen anfällig. Wenn Sie beide verwenden, werden Fehler abgefangen, die möglicherweise von dem einen oder anderen übersehen wurden, selbst in Code, der keine öffentlich zugängliche API ist. Beispielsweise hat möglicherweise 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, haben Sie mehr Chancen, den Fehler zu erkennen.

In der Informationssicherheit wird dies als Tiefenverteidigung bezeichnet. Wenn Sie mehrere Verteidigungsebenen haben, können Sie sicher sein, dass eine weitere Ebene sie fängt, wenn eine ausfällt.

Ihr Kollege hat in einer Sache Recht: Sie sollten Ihre Validierungen testen, aber dies ist keine "unnötige Arbeit". Es ist dasselbe wie beim Testen eines anderen Codes. Sie möchten sicherstellen, dass alle Verwendungen, auch ungültige, ein erwartetes Ergebnis haben.

32
Kevin Fee

TDD ersetzt absolut keine 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 geschrieben zu haben - 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 Schritt „Refactor“. Während TDD test -gefahren ist, bedeutet dies nicht test -nur. Sie sollten weiterhin das richtige Design anwenden und sinnvollen Code schreiben. Das Schreiben von defensivem Code ist sinnvoller Code, da dadurch die Erwartungen expliziter und Ihr Code insgesamt robuster werden. Wenn Sie mögliche Fehler frühzeitig erkennen, können Sie sie leichter debuggen.

Aber sollten wir nicht Tests verwenden, um Fehler zu lokalisieren? Aussagen 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 konzeptionelles Missverständnis Ihres Kollegen: Unit-Tests können niemals verwendet Ihrer Klasse testen, nur dass die Klasse selbst isoliert wie erwartet funktioniert. 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 Edge- und Fehlerfälle abdecken, eignen sich besser für Unit-Tests.

30
amon

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 nirgends zu sehen. Sie sind wie Gerüste, mit denen eine hohe Mauer oder eine Felskuppel errichtet wurde. Sie lassen wichtige Teile nicht aus der Struktur heraus, da sie während des Baus von einem Gerüst gehalten werden. Sie haben ein Gerüst, das es während des Baus 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 jedoch überflüssig oder sogar schädlich sein. Wenn Sie beispielsweise in die Kommentare intrinsisches Wissen über den Code einfügen und dann den Code ändern, werden die Kommentare im besten Fall irrelevant und im schlimmsten Fall schädlich.

Nehmen wir also an, Sie haben viel Wissen über Ihre Codebasis in die Tests gesteckt, z. B. kann MethodA keine Null annehmen und das Argument von MethodB muss > 0 Sein. Dann ändert sich der Code. Null ist jetzt für A in Ordnung, und B kann Werte von bis zu -10 annehmen. Die vorhandenen Tests sind jetzt funktional falsch, werden aber weiterhin bestanden.

Ja, Sie sollten die Tests gleichzeitig mit der Aktualisierung des Codes 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 überprüfen das Verhalten des Systems. Dieses tatsächliche Verhalten ist dem System selbst eigen, nicht den Tests eigen.

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 alle Tests bestanden werden.

Was bedeutet, dass defensive Programmierung ist der Punkt.

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 sich der Fehler manifestiert. Dann wird der Code korrigiert, mit Code, mit dem diese Tests bestanden werden, 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 Vorbedingungsprüfungen (defensive Programmierung) verwendet, um die an sie übergebenen Argumente zu bestätigen.

Generisch ...

Wenn beispielsweise ein Nullargument für eine bestimmte Prozedur ungültig ist, besteht mindestens ein Test eine Null und es wird eine Ausnahme/ein Fehler "ungültiges Nullargument" erwartet.

Mindestens ein anderer Test wird natürlich ein gültiges Argument bestehen - oder ein großes Array durchlaufen und unzählige gültige Argumente übergeben - und bestätigen, dass der resultierende Zustand angemessen ist.

Wenn ein Test dieses Nullargument nicht übergibt und mit der erwarteten Ausnahme geklatscht wird (und diese Ausnahme ausgelöst wurde, weil der Code den übergebenen Status defensiv überprüft hat it), dann kann die Null einer Eigenschaft einer Klasse zugewiesen oder in einer Sammlung vergraben werden, in der sie nicht sein sollte.

Dies kann zu unerwartetem Verhalten in einem völlig anderen Teil des Systems führen, an den die Klasseninstanz übergeben wird, in einem entfernten geografischen Gebietsschema , nachdem die Software ausgeliefert wurde . Und so etwas versuchen wir eigentlich zu vermeiden, oder?

Es könnte noch schlimmer sein. Die Klasseninstanz mit dem ungültigen Status kann 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 ist es ein mechanisches Steuerungssystem, 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 codiert haben.

16
Craig

Anstelle von TDD sprechen wir über "Softwaretests" im Allgemeinen und anstelle von "defensiver Programmierung" im Allgemeinen über meine bevorzugte Art der defensiven Programmierung, bei der Behauptungen verwendet werden.


Da wir also Softwaretests durchführen, sollten wir aufhören, assert-Anweisungen in den Produktionscode zu setzen, oder? Lassen Sie mich zählen, wie dies falsch ist:

  1. Zusicherungen sind optional. Wenn Sie sie nicht mögen, führen Sie Ihr System einfach mit deaktivierten Zusicherungen aus.

  2. Assertions überprüfen Dinge, die das Testen nicht kann (und sollte). 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.)

  3. 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 im Laufe der Entwicklung des Codes in der Regel veraltet und kann vom Compiler in keiner Weise durchgesetzt werden.

  4. 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?

  5. Behauptungen können relevanter sein als das Testen. Bei den Tests wird geprüft, was in den funktionalen Anforderungen vorgeschrieben ist. Der Code muss jedoch häufig bestimmte Annahmen treffen, die weitaus technischer sind. Menschen, die Dokumente zu funktionalen Anforderungen schreiben, denken selten an eine Division durch Null.

  6. Behauptungen zeigen Fehler auf, 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 ausreichender Fehlerbehebung werden Sie schließlich genau feststellen, wo etwas schief gelaufen ist, aber Behauptungen werden es normalerweise zuerst finden.

  7. Behauptungen reduzieren die Programmkomplexität. Jede einzelne Codezeile, die Sie schreiben, erhöht die Programmkomplexität. Assertions und das Schlüsselwort final (readonly) sind die einzigen zwei Konstrukte, von denen ich weiß, dass sie die Programmkomplexität tatsächlich reduzieren. Das ist unbezahlbar.

  8. Assertions helfen dem Compiler, Ihren Code besser zu verstehen. Bitte versuchen Sie dies zu Hause: void foo( Object x ) { assert x != null; if( x == null ) { } } Ihr Compiler sollte eine Warnung ausgeben, die Sie darüber informiert, dass die Bedingung x == null ist immer falsch. Das kann sehr nützlich sein.

Das Obige war eine Zusammenfassung eines Beitrags aus meinem Blog, 2014-09-21 "Assertions and Testing"

9
Mike Nakis

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 von Ihnen getesteten Anwendung verwendet? Wenn Sie eine Bibliothek oder API zur Verwendung durch Dritte bereitstellen, können Sie nicht sicherstellen, dass diese Ihren Code nur mit gültiger Eingabe aufrufen. Sie müssen alle Eingaben validieren.

Wenn das betreffende Modul jedoch nur von Code verwendet wird, den Sie steuern, hat Ihr Freund möglicherweise einen Punkt. Mithilfe von Komponententests können Sie überprüfen, ob das betreffende Modul nur mit gültiger Eingabe aufgerufen wird. Voraussetzungsprüfungen könnten immer noch als eine gute Praxis angesehen werden, aber es ist ein Kompromiss: Wenn Sie den Code wegwerfen, der nach Bedingungen sucht, die Sie wissen niemals entstehen können, verdeckt dies nur die Absicht des Codes.

Ich bin nicht der Meinung, dass Vorbedingungsprüfungen mehr Unit-Tests erfordern. Wenn Sie entscheiden, dass Sie einige Formen ungültiger Eingaben nicht testen müssen, sollte es keine Rolle spielen, ob die Funktion Vorbedingungsprüfungen enthält oder nicht. Denken Sie daran, dass Tests das Verhalten überprüfen sollten, nicht die Implementierungsdetails.

5
JacquesB

Dieses Argument verwirrt mich irgendwie, denn als ich anfing, TDD zu üben, erhöhten sich meine Komponententests der Form "Objekt reagiert auf <bestimmte Weise>, wenn <ungültige Eingabe>" zwei- oder dreimal erhöht wurden. Ich frage mich, wie es Ihrem Kollegen gelingt, solche Unit-Tests 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 übergeben werden, ist viel schwieriger zu beweisen. Wie im ersten Fall hängt es stark von der gründlichen Abdeckung von Edge-Fällen ab, aber Sie haben zusätzlich die Anforderung, dass alle Ihre Funktionseingaben von den Ausgängen anderer Funktionen stammen müssen, deren Ausgänge Sie als Einheit getestet haben, und nicht beispielsweise von Benutzereingaben oder Module von Drittanbietern.

Mit anderen Worten, was TDD tut, hindert Sie nicht daran, benötigt Validierungscode, sondern hilft Ihnen, vergessen zu vermeiden.

3
Karl Bielefeldt

Ich denke, ich interpretiere die Bemerkungen Ihres Kollegen anders als die meisten anderen Antworten.

Es scheint mir, dass das Argument ist:

  • Unser gesamter Code ist Unit-getestet.
  • Der gesamte Code, der Ihre Komponente verwendet, ist unser Code oder wird, falls nicht, von einer anderen Person als Unit-Test durchgeführt (nicht explizit angegeben, aber ich verstehe aus "Unit-Tests sollten falsche Verwendungen der Klasse abfangen").
  • Daher gibt es für jeden Aufrufer Ihrer Funktion irgendwo einen Komponententest, der Ihre Komponente verspottet, und der Test schlägt fehl, wenn der Aufrufer diesem Modell einen ungültigen Wert übergibt.
  • Daher spielt es keine Rolle, was Ihre Funktion tut, wenn ein ungültiger Wert übergeben wird, da unsere Tests besagen, dass dies nicht möglich ist.

Für mich hat dieses Argument eine gewisse Logik, verlässt sich jedoch zu sehr auf Komponententests, um jede mögliche Situation abzudecken. Die einfache Tatsache ist, dass eine 100% ige Abdeckung von Leitungen/Zweigen/Pfaden nicht unbedingt jeden Wert ausübt, den der Anrufer möglicherweise weitergibt, während eine 100% ige Abdeckung aller möglichen Zustände des Anrufers (dh) sind alle möglichen Werte seiner Eingaben und Variablen rechnerisch nicht realisierbar.

Daher würde ich es vorziehen, die Anrufer einem Unit-Test zu unterziehen, um sicherzustellen, dass sie (soweit die Tests gehen) niemals schlechte Werte übergeben, und zusätzlich zu verlangen, dass Ihre Komponente auf erkennbare Weise ausfällt, wenn Es wird ein schlechter Wert übergeben (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 weniger streng 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 sind nicht mehr ungültig (zumindest nicht ungültiger als alle anderen) Argument überhaupt zu throw, da auch dies dokumentiert ist, um eine Ausnahme auszulösen!). Anrufer sind berechtigt, sich auf dieses Abwehrverhalten zu verlassen. Wenn es die Sprache erlaubt, kann es sein, dass dies auf jeden Fall das beste Szenario ist - die Funktion hat keine "ungültigen Eingaben", aber Anrufer, die erwarten, die Funktion nicht zum Auslösen einer Ausnahme zu provozieren, sollten unit- sein. ausreichend getestet, um sicherzustellen, dass sie keine Werte übergeben, die dies verursachen.

Obwohl ich denke, dass Ihr Kollege etwas weniger gründlich falsch ist als die meisten Antworten, komme ich zu dem gleichen Schluss, dass sich die beiden Techniken ergänzen. Defensiv programmieren, Defensivprüfungen dokumentieren und testen. Die Arbeit ist nur "unnötig", wenn Benutzer Ihres Codes nicht von nützlichen Fehlermeldungen profitieren können, wenn sie Fehler machen. Theoretisch werden die Fehlermeldungen nie angezeigt, wenn sie ihren gesamten Code gründlich einem Unit-Test unterziehen, bevor sie ihn in Ihren integrieren, und niemals Fehler in ihren Tests auftreten. In der Praxis können sie selbst dann, wenn sie TDD und Total Dependency Injection durchführen, während der Entwicklung noch untersuchen oder es kann zu einem Zeitraffer bei ihren Tests kommen. Das Ergebnis ist, dass sie Ihren Code aufrufen, bevor ihr Code perfekt ist!

2
Steve Jessop

Eine gute Reihe von Tests wird die externe Schnittstelle Ihrer Klasse trainieren und sicherstellen, dass solche Missbräuche die richtige Antwort erzeugen (eine Ausnahme oder was auch immer Sie als "richtig" definieren). Tatsächlich besteht der erste Testfall, den ich für eine Klasse schreibe, darin, ihren Konstruktor mit Argumenten außerhalb des Bereichs aufzurufen.

Die Art der defensiven Programmierung, die durch einen vollständig auf Einheiten getesteten Ansatz eliminiert wird, ist die unnötige Validierung von internen Invarianten, die nicht durch externen Code verletzt werden können.

Eine nützliche Idee, die ich manchmal verwende, ist die Bereitstellung einer Methode, mit der die Invarianten des Objekts getestet werden. Ihre Teardown-Methode kann es aufrufen, um zu überprüfen, ob Ihre externen Aktionen für das Objekt die Invarianten niemals zerstören.

1
Toby Speight

Tests definieren den Vertrag Ihrer Klasse.

Als Konsequenz definiert das Fehlen eines Tests einen Vertrag, der enthält ndefiniertes Verhalten . Wenn Sie also null an Foo::Frobnicate(Widget widget) übergeben und unermessliche Laufzeitprobleme auftreten, befinden Sie sich immer noch im Vertrag Ihrer Klasse.

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 haben müssen, um null an Foo::Frobnicate(Widget widget) zu übergeben.

Und Sie dokumentieren diese Entscheidung, indem Sie a

[Test]
void Foo_FrobnicatesANullWidget_ThrowsInvalidArgument() 
{
    Given(Foo foo);
    When(foo.Frobnicate(null));
    Then(Expect_Exception(InvalidArgument));
}
1
Caleth

Die Verteidigung gegen Missbrauch ist eine Funktion , die aufgrund einer Anforderung entwickelt wurde. (Nicht alle Schnittstellen erfordern strenge Überprüfungen auf Missbrauch, zum Beispiel sehr eng verwendete interne.)

Die Funktion muss getestet werden: Funktioniert die Abwehr gegen Missbrauch tatsächlich? Das Ziel des Testens dieser Funktion besteht darin, zu zeigen, dass dies nicht der Fall ist: einen Missbrauch des Moduls zu erfinden, 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 ein Merkmal einer Funktion handelt, die (sagen wir) eine Ausnahme auslöst, wenn Parameter drei negativ ist, ist dies nicht verhandelbar. es soll das tun.

Ich vermute jedoch, dass Ihr Kollege tatsächlich unter dem Gesichtspunkt einer Situation Sinn macht, in der keine spezifischen Überprüfungen von Eingaben erforderlich sind, mit spezifischen Antworten auf schlechte Eingaben: eine Situation, in der dies der Fall ist ist nur eine verstandene allgemeine Voraussetzung für Robustheit.

Überprüfungen beim Eintritt in eine Funktion der obersten Ebene dienen teilweise dazu, schwachen oder schlecht getesteten internen Code vor unerwarteten Kombinationen von Parametern zu schützen (so dass die Überprüfungen nicht erforderlich sind, wenn der Code gut getestet ist: 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 Stücken niedrigerer Ebene aufbauen, die defensiv codiert und individuell gegen jeden Missbrauch getestet werden, ist es möglich, dass die Funktion höherer Ebene funktioniert robust ohne eigene umfangreiche Selbsttests.

Wenn gegen seinen Vertrag verstoßen wird, führt dies zu einem Missbrauch der Funktionen auf niedrigerer Ebene, möglicherweise durch Auslösen von Ausnahmen oder was auch immer.

Das einzige Problem dabei ist, dass die Ausnahmen auf niedrigerer Ebene nicht spezifisch für die Schnittstelle auf höherer 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 eher eine Ausnahme als einen Absturz auslösen oder weiterhin mit Mülldaten rechnen", kann sie tatsächlich durch die Robustheit der Teile der unteren Ebene abgedeckt werden, auf denen sie sich befindet gebaut.

Wenn für die Funktion eine sehr spezifische, detaillierte Fehlerberichterstattung in Bezug auf ihre Parameter erforderlich ist, erfüllen die Prüfungen auf niedrigerer Ebene diese Anforderungen nicht vollständig. Sie stellen nur sicher, dass die Funktion irgendwie explodiert (wird nicht mit einer schlechten Kombination von Parametern fortgesetzt, was zu einem Müllergebnis führt). Wenn der Client-Code so geschrieben ist, dass er bestimmte Fehler abfängt und behandelt, funktioniert er möglicherweise nicht richtig. Der Client-Code kann selbst als Eingabe die Daten abrufen, auf denen die Parameter basieren, und er kann erwarten, dass die Funktion diese überprüft und fehlerhafte Werte in die dokumentierten spezifischen Fehler übersetzt (damit er diese verarbeiten kann) Fehler richtig) anstatt einiger anderer Fehler, die nicht behandelt werden und möglicherweise das Software-Image stoppen.

TL; DR: Ihr Kollege ist wahrscheinlich kein Idiot. Sie sprechen 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 codieren. Der Kollege denkt, lass einfach den robusten Code der unteren Ebene explodieren, wenn die Parameter falsch sind. Es ist etwas unproduktiv, über ungeschriebene Anforderungen im Code zu streiten: Erkennen Sie, dass Sie nicht mit Anforderungen, sondern mit Code nicht einverstanden sind. Ihre Art der Codierung spiegelt wider, was Ihrer Meinung nach die Anforderungen sind. Der Weg des Kollegen repräsentiert seine Sicht auf die Anforderungen. Wenn Sie es so sehen, ist es klar, dass das, was richtig oder falsch ist, nicht im Code selbst enthalten ist. Der Code ist nur ein Proxy für Ihre Meinung zu der Spezifikation.

1
Kaz

Öffentliche Schnittstellen können und werden missbraucht

Die Behauptung Ihres Mitarbeiters "Unit-Tests sollten falsche Verwendungen der Klasse abfangen" ist für jede nicht private Schnittstelle streng falsch. Wenn eine öffentliche Funktion mit ganzzahligen Argumenten aufgerufen werden kann, kann und wird sie mit ganzzahligen Argumenten any aufgerufen, und der Code sollte sich entsprechend verhalten. Wenn eine öffentliche Funktionssignatur z.B. Java Doppeltyp, dann null, NaN, MAX_VALUE, -Inf sind alle möglichen Werte. Ihre Komponententests können keine falschen Verwendungen der Klasse abfangen, da diese Tests den Code nicht testen können, der diese Klasse verwendet. da dieser Code noch nicht geschrieben wurde, möglicherweise nicht von Ihnen geschrieben wurde und definitiv außerhalb des Bereichs von your Unit-Tests liegt.

Andererseits kann dieser Ansatz für die (hoffentlich viel zahlreicheren) privaten Eigenschaften gelten - wenn eine Klasse sicherstellen kann, dass eine Tatsache immer wahr ist (z. B. kann Eigenschaft X niemals null sein, ganzzahlige Position überschreitet nicht die maximale Länge, wenn Funktion A aufgerufen wird, alle erforderlichen Datenstrukturen gut geformt sind), kann es angebracht sein, dies aus Leistungsgründen nicht immer wieder zu überprüfen und sich stattdessen auf Komponententests zu verlassen.

1
Peteris

Die Tests von TDD werden Fehler während der Entwicklung des Codes auffangen.

Die Grenzen, die Sie als Teil der defensiven Programmierung beschreiben, werden Fehler während der Verwendung des Codes auffangen.

Wenn die beiden Domänen identisch sind, dh der Code, den Sie schreiben, wird immer nur intern von diesem bestimmten Projekt verwendet, kann es sein, dass TDD die Notwendigkeit der von Ihnen beschriebenen Überprüfung der defensiven Programmiergrenzen ausschließt, aber nur, wenn - Diese Arten der Grenzwertprüfung werden speziell in TDD-Tests durchgeführt..


Angenommen, eine Bibliothek mit Finanzcodes wurde mithilfe von TDD entwickelt. 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, sie ist verfügbar). Grenzüberprüfung würde.

Mein Punkt ist, dass eine TDD-Zusicherung zwar das Problem des negativen Werts lösen könnte, wenn der Code nur intern im Rahmen der Entwicklung einer größeren Anwendung (unter TDD) verwendet wird, wenn es sich jedoch um eine Bibliothek handelt, die von anderen Programmierern verwendet wird ohne das TDD-Framework und die Tests, begrenzt die Überprüfung.

0
Blackhawk

TDD und defensive Programmierung gehen Hand in Hand. Die Verwendung von beiden ist nicht redundant, 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 beschreiben, was passiert, wenn eine schlechte Eingabe, eine schlechte Rückgabe, ein schlechter Zustand usw. vorliegen, schreiben Sie Ihre Tests nicht robust genug, und Ihr Code ist selbst dann fragil, wenn alle Ihre Tests bestanden wurden.

Als Embedded Engineer verwende ich gerne das Beispiel des Schreibens einer Funktion, um einfach zwei Bytes zu addieren und das Ergebnis wie folgt zurückzugeben:

uint8_t AddTwoBytes(uint8_t a, uint8_t b, uint8_t *sum); 

Wenn Sie einfach *(sum) = a + b gemacht hätten, würde es funktionieren, aber nur mit einigen Eingaben. a = 1 Und b = 2 Würden sum = 3 Machen; Da die Größe der Summe jedoch ein Byte ist, würden a = 100 und b = 200 aufgrund eines Überlaufs sum = 44 ergeben. 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 Sie die Fehler nicht berücksichtigen oder nicht testen, wie sie behandelt werden sollen, funktioniert dies langfristig nicht, da sie unter diesen Bedingungen nicht behandelt werden und eine Reihe von Problemen verursachen können.

0
Dom