Macht TDD defensive Programmierung überflüssig?


104

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?


120
Sie geben Ihre vollständig Unit-getestete Bibliothek ohne Konstruktorprüfungen an einen Client weiter, um sie zu verwenden, und dieser bricht den Klassenvertrag. Was nützen dir diese Unit-Tests jetzt?
Robert Harvey

42
IMO ist es umgekehrt. Defensive Programmierung, korrekte Vor- und Voraussetzungen und ein reichhaltiges Typensystem machen Tests überflüssig.
Gardenhead

37
Kann ich eine Antwort hinterlassen, die nur "Gute Trauer" sagt? Defensive Programmierung schützt das System zur Laufzeit. Die Tests überprüfen alle möglichen Laufzeitbedingungen, die dem Tester einfallen können, einschließlich ungültiger Argumente, die an Konstruktoren und andere Methoden übergeben wurden. Wenn die Tests abgeschlossen sind, wird bestätigt, dass das Laufzeitverhalten wie erwartet ist, einschließlich der entsprechenden Ausnahmen, die ausgelöst werden oder anderes absichtliches Verhalten, das auftritt, wenn ungültige Argumente übergeben werden. Die Tests tragen jedoch nicht zum Schutz des Systems zur Laufzeit bei.
Craig

16
"Komponententests sollten falsche Verwendungen der Klasse auffangen" - äh, wie? Unit-Tests zeigen Ihnen das Verhalten bei korrekten und bei falschen Argumenten. Sie können nicht alle Argumente aufzeigen, die jemals vorgebracht werden.
OJFord

34
Ich glaube nicht, dass ich ein besseres Beispiel dafür gesehen habe, wie dogmatisches Denken über Softwareentwicklung zu schädlichen Schlussfolgerungen führen kann.
Sdenham

Antworten:


196

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.

Oder ergänzen sich die beiden Techniken?

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


7
Ich bin mit der Überzeugung nicht einverstanden, dass die Voraussetzungsvalidierung getestet werden sollte, aber ich bin mit der Meinung meines Kollegen nicht einverstanden, dass die zusätzliche Arbeit, die durch die Notwendigkeit verursacht wird, die Voraussetzungsvalidierung zu testen, ein Argument ist, die Voraussetzungsvalidierung in der ersten nicht zu erstellen Ort. Ich habe meinen Beitrag zur Klarstellung bearbeitet.
user2180613

20
@ user2180613 Erstellen Sie einen Test, der prüft, ob ein Fehler der Vorbedingung ordnungsgemäß behandelt wurde: Jetzt ist das Hinzufügen des Checks keine "zusätzliche" Arbeit, sondern die Arbeit, die TDD benötigt, um den Test grün zu machen. Wenn Ihr Kollege der Meinung ist, dass Sie den Test durchführen sollten, wenn Sie feststellen, dass er fehlschlägt, und dann und erst dann die Voraussetzungsprüfung durchführen, hat er möglicherweise einen Punkt aus TDD-puristischer Sicht. Wenn er sagt, nur um den Scheck komplett zu ignorieren, dann ist er albern. In TDD gibt es nichts, was besagt, dass Sie beim Schreiben von Tests für potenzielle Fehlermodi nicht proaktiv sein können.
RM

4
@RM Sie schreiben keinen Test, um die Voraussetzungsprüfung zu testen. Sie schreiben einen Test, um das erwartete korrekte Verhalten des aufgerufenen Codes zu testen. Die Voraussetzungsprüfungen sind aus Sicht des Tests ein undurchsichtiges Implementierungsdetail, das das richtige Verhalten sicherstellt. Wenn Sie sich einen besseren Weg überlegen, um den richtigen Zustand im aufgerufenen Code sicherzustellen, tun Sie dies auf diese Weise, anstatt eine herkömmliche Voraussetzungsprüfung durchzuführen. Der Test wird zeigen, ob Sie erfolgreich waren oder nicht, und es wird Sie immer noch nicht interessieren, wie Sie es getan haben.
Craig

@ user2180613 Das ist eine großartige Rechtfertigung: D Wenn Ihr Ziel beim Schreiben von Software darin besteht, die Anzahl der Tests zu verringern, die Sie erstellen und ausführen müssen, schreiben Sie keine Software - keine Tests!
Gusdor,

3
Der letzte Satz dieser Antwort nagelt es.
Robert Grant

32

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.


Ist es richtig zu sagen, dass die Parametervalidierung eine Form der Vorbedingungsvalidierung ist und Komponententests Nachbedingungsvalidierungen sind, weshalb sie sich ergänzen?
user2180613

1
"Es ist das gleiche wie beim Testen von anderem Code. Sie möchten sicherstellen, dass alle Verwendungen, auch ungültige, ein erwartetes Ergebnis haben." Diese. Kein Code sollte jemals nur durchgelassen werden, wenn seine übergebene Eingabe nicht dafür ausgelegt ist. Dies verstößt gegen das "Fail Fast" -Prinzip und kann das Debuggen zu einem Albtraum machen.
jpmc26

@ user2180613 - Nicht wirklich, aber diese Unit-Tests prüfen, ob Fehlerbedingungen vorliegen, die der Entwickler erwartet, während defensive Programmiertechniken nach Bedingungen suchen, die der Entwickler nicht erwartet. Unit-Tests können verwendet werden, um Vorbedingungen zu validieren (indem ein Scheinobjekt verwendet wird, das dem Aufrufer injiziert wird, der die Vorbedingungen überprüft).
Periata Breatta

1
@ jpmc26 Ja, der Fehler ist das "erwartete Ergebnis" für den Test. Sie testen, um zu zeigen, dass es fehlschlägt, anstatt im Hintergrund ein undefiniertes (unerwartetes) Verhalten zu zeigen.
KRyan

6
TDD erkennt Fehler in Ihrem eigenen Code, defensive Programmierung erkennt Fehler im Code anderer Personen. TDD kann so dazu beitragen, dass Sie defensiv genug sind :)
jwenting

30

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.


16

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.


2
Das ist lustig, die Abstimmung ging so schnell, dass der Downvoter möglicherweise über den ersten Absatz hinaus hätte lesen können.
Craig

1
:-) Ich habe gerade ohne Lektüre über den ersten Absatz hinaus abgestimmt, also hoffentlich wird das den Ausgleich schaffen ...
SusanW

1
Schien das Mindeste , was ich tun konnte :-) (Eigentlich habe ich habe nur , um sicherzustellen , muss nicht nachlässig sein , den Rest zu lesen -.! Besonders auf ein Thema wie dieses)
SusanW

1
Ich dachte du hättest es wahrscheinlich. :)
Craig

Defensive Checks können zur Kompilierungszeit mit Tools wie Code Contracts durchgeführt werden.
Matthew Whited

9

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:

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

  2. 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.)

  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 mit der Entwicklung des Codes in der Regel veraltet und für den Compiler in keiner Weise durchsetzbar.

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

  6. 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.

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

  8. 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 Bedingung x == nullimmer falsch ist. Das kann sehr nützlich sein.

Das obige war eine Zusammenfassung eines Beitrags aus meinem Blog, 21.09.2014 "Behauptungen und Tests"


Ich glaube, ich bin mit dieser Antwort größtenteils nicht einverstanden. (5) In TDD ist die Testsuite die Spezifikation. Sie sollten den einfachsten Code schreiben, der die Tests besteht, sonst nichts. (4) Der rot-grüne Workflow stellt sicher, dass der Test fehlschlägt und bestanden wird, wenn die beabsichtigte Funktionalität vorhanden ist. Behauptungen helfen hier nicht viel. (3,7) Dokumentation ist Dokumentation, Behauptungen nicht. Indem jedoch Annahmen explizit gemacht werden, wird der Code selbstdokumentierender. Ich würde sie als ausführbare Kommentare betrachten. (2) White-Box-Tests können Teil einer gültigen Teststrategie sein.
amon

5
"In TDD ist die Testsuite die Spezifikation. Sie sollten den einfachsten Code schreiben, der die Tests besteht, sonst nichts.": Ich denke nicht, dass dies immer eine gute Idee ist: Wie in der Antwort erwähnt, gibt es zusätzliche interne Annahme im Code, die überprüft werden soll. Was ist mit internen Fehlern, die sich gegenseitig auslöschen? Ihre Tests bestehen, aber einige Annahmen in Ihrem Code sind falsch, was später zu heimtückischen Fehlern führen kann.
Giorgio

5

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.


4
Wenn die aufgerufene Prozedur die Gültigkeit der Eingaben nicht überprüft (dies ist die ursprüngliche Debatte), können Ihre Komponententests nicht sicherstellen, dass das betreffende Modul nur mit einer gültigen Eingabe aufgerufen wird. Insbesondere kann es vorkommen, dass ein Aufruf mit ungültiger Eingabe erfolgt, in den getesteten Fällen jedoch nur ein korrektes Ergebnis zurückgegeben wird - es gibt verschiedene Arten von undefiniertem Verhalten, Überlaufbehandlung usw., die in einer Testumgebung mit deaktivierten Optimierungen jedoch das erwartete Ergebnis zurückgeben können in der Produktion scheitern.
Peteris

@Peteris: Denkst du an undefiniertes Verhalten wie in C? Das Aufrufen von undefiniertem Verhalten, das in verschiedenen Umgebungen zu unterschiedlichen Ergebnissen führt, ist offensichtlich ein Fehler, der jedoch auch nicht durch Voraussetzungsprüfungen verhindert werden kann. Wie prüft man beispielsweise, ob ein Zeigerargument auf einen gültigen Speicher verweist?
JacquesB

3
Dies funktioniert nur in den kleinsten Läden. Wenn Ihr Team beispielsweise sechs Personen hinter sich lässt, müssen Sie die Validierungsprüfungen trotzdem durchführen.
Robert Harvey

1
@RobertHarvey: In diesem Fall sollte das System in Subsysteme mit genau definierten Schnittstellen aufgeteilt und die Eingabevalidierung an der Schnittstelle durchgeführt werden.
JacquesB

diese. Es kommt auf den Code an. Soll dieser Code vom Team verwendet werden? Hat das Team Zugriff auf den Quellcode? Wenn der rein interne Code, der dann nach Argumenten sucht, nur eine Belastung ist, prüfen Sie beispielsweise, ob eine Ausnahme vorliegt, und der Aufrufer prüft den Code, ob diese Klasse eine Ausnahme usw. usw. auslösen kann, und wartet in diesem Fall Das Objekt wird niemals 0 erhalten, da es zuvor 2 Level herausgefiltert hat. Wenn das ein Bibliothekscode ist, der von Dritten verwendet werden soll, ist das eine andere Geschichte. Nein, der gesamte Code ist für die Verwendung durch die ganze Welt geschrieben.
Aleksander Fular

3

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 .


2

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

Mir scheint das Argument zu sein:

  • Alle unsere Codes sind einheitlich getestet.
  • Der gesamte Code, der Ihre Komponente verwendet, ist unser Code oder, falls nicht, von einer anderen Person getestet worden (nicht explizit angegeben, aber ich verstehe, dass Unit-Tests alle falschen Verwendungen der Klasse erfassen sollten).
  • 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 einen ungültigen Wert an diesen Verspott ü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, 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 throwist) 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!


Das Geschäft, bei dem der Schwerpunkt auf das Testen der Anrufer gelegt wird, um sicherzustellen, dass sie keine schlechten Werte übergeben, scheint sich für fragilen Code mit vielen Bass-Ackwards-Abhängigkeiten und ohne saubere Trennung von Bedenken zu eignen. Ich glaube wirklich nicht, dass mir der Code gefällt, der sich aus dem Denken hinter diesem Ansatz ergeben würde.
Craig

@Craig: Wenn Sie eine Komponente zum Testen isoliert haben, indem Sie ihre Abhängigkeiten verspotten, warum sollten Sie dann nicht testen, dass sie nur diesen Abhängigkeiten die richtigen Werte übergibt? Und wenn Sie die Komponente nicht isolieren können, haben Sie die Bedenken wirklich getrennt? Ich bin mit defensiver Codierung nicht einverstanden, aber wenn defensive Prüfungen die Mittel sind, mit denen Sie die Richtigkeit des Aufrufs von Code testen, dann ist das ein Durcheinander. Ich denke also, der Kollege des Fragestellers hat Recht, dass die Prüfungen überflüssig sind, aber es ist falsch, dies als Grund anzusehen, sie nicht zu schreiben :-)
Steve Jessop

Das einzige krasse Loch, das ich sehe, ist, dass ich immer noch teste, dass meine eigenen Komponenten keine ungültigen Werte an diese Abhängigkeiten weitergeben können. Ich bin vollkommen einverstanden, dass dies getan wird Komponente öffentlich, so dass Partner es nennen können? Das erinnert mich tatsächlich an das Datenbankdesign und die ganze derzeitige Liebesbeziehung zu ORMs, was dazu führt, dass so viele (meist jüngere) Leute erklären, dass Datenbanken nur ein blöder Netzwerkspeicher sind und sich nicht mit Einschränkungen, Fremdschlüsseln und gespeicherten Prozeduren schützen sollten.
Craig

Das andere, was ich sehe, ist natürlich, dass Sie in diesem Szenario nur Aufrufe zu Verspottungen testen, nicht zu den tatsächlichen Abhängigkeiten. Letztendlich ist es der Code in diesen Abhängigkeiten, der mit einem bestimmten übergebenen Wert ordnungsgemäß funktionieren kann oder nicht, nicht der Code im Aufrufer. Die Abhängigkeit muss also das Richtige tun, und es muss eine ausreichende unabhängige Testabdeckung der Abhängigkeit vorhanden sein, um dies sicherzustellen. Denken Sie daran, diese Tests, von denen wir sprechen, werden "Einheitentests" genannt. Jede Abhängigkeit ist eine Einheit. :)
Craig

1

Ö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.


Die Überschrift und der erste Absatz sind richtig, da nicht die Komponententests den Code zur Laufzeit ausführen. Es ist egal, welcher andere Laufzeitcode und sich ändernde reale Bedingungen sowie schlechte Benutzereingaben und Hacking-Versuche mit dem Code interagieren.
Craig

1

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.


Dies hängt mit einer allgemeinen philosophischen Schwierigkeit zusammen, mit möglicherweise losen Anforderungen umzugehen. Wenn es einer Funktion erlaubt ist, sich signifikant, aber nicht vollständig frei zu verhalten, kann sie sich bei fehlerhaften Eingaben willkürlich verhalten (z. B. wenn ein Bilddecoder möglicherweise die Anforderungen erfüllt, wenn garantiert werden kann, dass er - nach Belieben - eine willkürliche Pixelkombination erzeugt oder abnormal endet Es kann jedoch unklar sein, welche Testfälle geeignet sind, um sicherzustellen, dass keine Eingaben ein inakzeptables Verhalten hervorrufen.
Supercat

1

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.nullFoo::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 , nullzu Foo::Frobnicate(Widget widget).

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

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.


0

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.


1
Ich habe nicht abgelehnt, aber ich stimme mit den Ablehnungen überein, dass das Hinzufügen subtiler Unterscheidungen zu dieser Art von Argument das Wasser trübt.
Craig

@Craig Ich wäre an Ihrem Feedback zu dem von mir hinzugefügten Beispiel interessiert.
Blackhawk

Mir gefällt die Besonderheit des Beispiels. Die einzige Sorge, die ich noch habe, betrifft das gesamte Argument. Zum Beispiel; Mit dabei ist ein neuer Entwickler im Team, der eine neue Komponente schreibt, die dieses Finanzmodul verwendet. Der Neue ist sich nicht aller Komplikationen des Systems bewusst, geschweige denn, dass alle Arten von Expertenwissen darüber, wie das System funktionieren soll, in die Tests eingebettet sind und nicht in den zu testenden Code.
Craig

Das heißt, der neue Mitarbeiter / die neue Mitarbeiterin hat einige wichtige Tests nicht erstellt, und es kommt zu Redundanz bei den Tests. Tests in verschiedenen Teilen des Systems überprüfen die gleichen Bedingungen und werden im Laufe der Zeit inkonsistent, anstatt nur die entsprechenden Aussagen zu treffen und Die Vorbedingung überprüft den Code, in dem sich die Aktion befindet.
Craig

1
Sowas in der Art. Abgesehen davon, dass viele der hier vorgebrachten Argumente sich darauf beziehen, dass die Tests für den aufrufenden Code alle Überprüfungen durchführen. Wenn Sie jedoch überhaupt ein gewisses Maß an Fan-In haben, führen Sie dieselben Überprüfungen an verschiedenen Orten durch, und das ist ein Wartungsproblem an sich. Was ist, wenn sich der Bereich gültiger Eingaben für eine Prozedur ändert, Sie jedoch das Domänenwissen für diesen Bereich in Tests integriert haben, in denen verschiedene Komponenten getestet werden? Ich bin immer noch voll und ganz für die defensive Programmierung und nutze die Profilerstellung, um festzustellen, ob und wann Leistungsprobleme zu beheben sind.
Craig

0

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:

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

Wenn Sie es einfach *(sum) = a + bmachen würden, würde es funktionieren, aber nur mit einigen Eingaben. a = 1und b = 2würde machen sum = 3; jedoch , weil die Größe der Summe ist ein Byte, a = 100und b = 200würde sum = 44aufgrund ü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.


Das sieht aus wie ein gutes Interview-Frage-Beispiel (warum hat es einen Rückgabewert und einen "out" -Parameter - und was passiert, wenn sumein Nullzeiger ist?).
Toby Speight
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.