web-dev-qa-db-fra.com

TDD rend-il la programmation défensive redondante?

Aujourd'hui, j'ai eu une discussion intéressante avec un collègue.

Je suis un programmeur défensif. Je crois que la règle "ne classe doit s'assurer que ses objets ont un état valide lorsqu'ils interagissent avec l'extérieur de la classe" doit toujours être respectée. La raison de cette règle est que la classe ne sait pas qui sont ses utilisateurs et qu'elle devrait échouer de manière prévisible lorsqu'elle est interagie de manière illégale. À mon avis, cette règle s'applique à toutes les classes.

Dans la situation spécifique où j'ai eu une discussion aujourd'hui, j'ai écrit du code qui valide que les arguments de mon constructeur sont corrects (par exemple, un paramètre entier doit être> 0) et si la condition préalable n'est pas remplie, une exception est levée. Mon collègue, en revanche, pense qu'un tel contrôle est redondant, car les tests unitaires devraient détecter toute utilisation incorrecte de la classe. De plus, il estime que les validations de programmation défensive devraient également être testées à l'unité, de sorte que la programmation défensive ajoute beaucoup de travail et n'est donc pas optimale pour TDD.

Est-il vrai que TDD est capable de remplacer la programmation défensive? La validation des paramètres (et je ne veux pas dire entrée utilisateur) est-elle par conséquent inutile? Ou les deux techniques se complètent-elles?

105
user2180613

C'est ridicule. TDD force le code à passer des tests et force tout le code à avoir des tests autour de lui. Cela n'empêche pas vos consommateurs d'appeler du code de manière incorrecte, ni n'empêche comme par magie les programmeurs de manquer des cas de test.

Aucune méthodologie ne peut forcer les utilisateurs à utiliser correctement le code.

Il y a = un léger argument à faire valoir que si vous aviez parfaitement fait le TDD, vous auriez intercepté votre vérification> 0 dans un cas de test, avant de l'implémenter, et corrigé cela - probablement en ajoutant la vérification . Mais si vous avez fait TDD, votre exigence (> 0 dans le constructeur) premier apparaîtra comme un cas de test qui échoue. Vous donnant ainsi le test après avoir ajouté votre chèque.

Il est également raisonnable de tester certaines des conditions défensives (vous avez ajouté de la logique, pourquoi ne voudriez-vous pas tester quelque chose d'aussi facilement testable?). Je ne sais pas pourquoi vous semblez en désaccord avec cela.

Ou les deux techniques se complètent-elles?

TDD développera les tests. L'implémentation de la validation des paramètres les fera passer.

196
enderland

La programmation défensive et les tests unitaires sont deux façons différentes de détecter les erreurs et ont chacune des forces différentes. L'utilisation d'une seule façon de détecter les erreurs fragilise vos mécanismes de détection d'erreurs. L'utilisation des deux détectera les erreurs qui auraient pu être manquées par l'un ou l'autre, même dans du code qui n'est pas une API publique; par exemple, quelqu'un peut avoir oublié d'ajouter un test unitaire pour les données non valides transmises à l'API publique. Tout vérifier aux endroits appropriés signifie plus de chances de détecter l'erreur.

En matière de sécurité de l'information, cela s'appelle Défense en profondeur. Le fait d'avoir plusieurs couches de défense garantit qu'en cas d'échec, il y en aura d'autres pour l'attraper.

Votre collègue a raison sur une chose: vous devriez tester vos validations, mais ce n'est pas un "travail inutile". C'est la même chose que de tester tout autre code, vous voulez vous assurer que toutes les utilisations, même invalides, ont un résultat attendu.

32
Kevin Fee

TDD ne remplace absolument pas la programmation défensive. Au lieu de cela, vous pouvez utiliser TDD pour vous assurer que toutes les défenses sont en place et fonctionnent comme prévu.

Dans TDD, vous n'êtes pas censé écrire du code sans avoir d'abord écrit un test - suivez religieusement le cycle rouge – vert – refactor. Cela signifie que si vous souhaitez ajouter une validation, écrivez d'abord un test qui nécessite cette validation. Appelez la méthode en question avec des nombres négatifs et avec zéro, et attendez-vous à ce qu'elle lève une exception.

N'oubliez pas non plus l'étape "refactoriser". Alors que TDD est test -piloté, cela ne signifie pas test -niquement. Vous devez toujours appliquer une conception appropriée et écrire du code sensible. Écrire du code défensif est un code sensé, car il rend les attentes plus explicites et votre code globalement plus robuste - repérer les erreurs possibles au début les rend plus faciles à déboguer.

Mais ne sommes-nous pas censés utiliser des tests pour localiser les erreurs? Les assertions et les tests sont complémentaires. Une bonne stratégie de test mélange de diverses approches pour s'assurer que le logiciel est robuste. Seuls les tests unitaires ou les tests d'intégration ou uniquement les assertions dans le code ne sont pas tous satisfaisants, vous avez besoin d'une bonne combinaison pour atteindre un degré de confiance suffisant dans votre logiciel avec un effort acceptable.

Ensuite, il y a un très gros malentendu conceptuel de votre collègue: les tests unitaires ne peuvent jamais tester tilise de votre classe, seulement que la classe elle-même fonctionne comme prévu de manière isolée. Vous utiliseriez des tests d'intégration pour vérifier que l'interaction entre les différents composants fonctionne, mais l'explosion combinatoire des cas de test possibles rend impossible tout tester. Les tests d'intégration devraient donc se limiter à quelques cas importants. Des tests plus détaillés qui couvrent également les cas Edge et les cas d'erreur conviennent mieux aux tests unitaires.

30
amon

Les tests sont là pour soutenir et assurer une programmation défensive

La programmation défensive protège l'intégrité du système lors de l'exécution.

Les tests sont des outils de diagnostic (principalement statiques). Au moment de l'exécution, vos tests ne sont nulle part en vue. Ils sont comme des échafaudages utilisés pour ériger un haut mur de briques ou un dôme rocheux. Vous ne laissez pas de parties importantes hors de la structure car vous avez un échafaudage qui la maintient pendant la construction. Vous avez un échafaudage qui le maintient pendant la construction pour faciliter la mise en place de toutes les pièces importantes.

EDIT: une analogie

Qu'en est-il d'une analogie avec les commentaires dans le code?

Les commentaires ont leur raison d'être, mais peuvent être redondants ou même nuisibles. Par exemple, si vous mettez des connaissances intrinsèques sur le code dans les commentaires , puis modifiez le code, les commentaires deviennent au mieux non pertinents et au pire nuisibles.

Supposons donc que vous ayez mis beaucoup de connaissances intrinsèques sur votre base de code dans les tests, comme MethodA ne peut pas accepter une valeur nulle et que l'argument de MethodB doit être > 0. Ensuite, le code change. Null est correct pour A maintenant, et B peut prendre des valeurs aussi petites que -10. Les tests existants sont maintenant fonctionnellement incorrects, mais continueront de passer.

Oui, vous devez mettre à jour les tests en même temps que vous mettez à jour le code. Vous devez également mettre à jour (ou supprimer) les commentaires en même temps que vous mettez à jour le code. Mais nous savons tous que ces choses ne se produisent pas toujours et que des erreurs sont commises.

Les tests vérifient le comportement du système. Ce comportement réel est intrinsèque au système lui-même, pas intrinsèque aux tests.

Qu'est ce qui pourrait aller mal?

Le but en ce qui concerne les tests est de trouver tout ce qui pourrait mal tourner, d'écrire un test pour cela qui vérifie le bon comportement, puis de créer le code d'exécution pour qu'il passe tous les tests.

Ce qui signifie que la programmation défensive est le point.

TDD pilote la programmation défensive, si les tests sont complets.

Plus de tests, conduisant à une programmation plus défensive

Lorsque des bogues sont inévitablement trouvés, d'autres tests sont écrits pour modéliser les conditions qui manifestent le bogue. Ensuite, le code est corrigé, avec du code pour que ces tests réussissent, et les nouveaux tests restent dans la suite de tests.

Un bon ensemble de tests va passer des arguments bons et mauvais à une fonction/méthode et s'attendre à des résultats cohérents. Ceci, à son tour, signifie que le composant testé va utiliser des vérifications de précondition (programmation défensive) pour confirmer les arguments qui lui sont transmis.

De manière générique ...

Par exemple, si un argument null à une procédure particulière n'est pas valide, alors au moins un test va passer un null, et il va s'attendre à une exception/erreur "argument null invalide" de quelque sorte.

Au moins un autre test va passer un argument valide, bien sûr - ou parcourir un grand tableau et passer une dizaine d'arguments valides - et confirmer que l'état résultant est approprié.

Si un test ne passe pas cet argument nul et est giflé avec l'exception attendue (et cette exception a été levée parce que le code a vérifié de manière défensive l'état transmis à ), alors le null peut finir par être assigné à une propriété d'une classe ou enterré dans une collection d'une sorte où il ne devrait pas être.

Cela peut provoquer un comportement inattendu dans une partie entièrement différente du système vers laquelle l'instance de classe est transmise, dans des paramètres régionaux géographiques distants après la livraison du logiciel . Et c'est le genre de chose que nous essayons réellement d'éviter, non?

Ça pourrait même être pire. L'instance de classe avec l'état non valide peut être sérialisée et stockée, uniquement pour provoquer un échec lorsqu'elle est reconstituée pour être utilisée ultérieurement. Bon sang, je ne sais pas, c'est peut-être un système de contrôle mécanique qui ne peut pas redémarrer après un arrêt car il ne peut pas désérialiser son propre état de configuration persistant. Ou l'instance de classe pourrait être sérialisée et transmise à un système entièrement différent créé par une autre entité, et ce système pourrait se bloquer.

Surtout si les programmeurs de cet autre système n'ont pas codé de manière défensive.

16
Craig

Au lieu de TDD, parlons des "tests de logiciels" en général, et au lieu de "programmation défensive" en général, parlons de ma façon préférée de faire de la programmation défensive, qui consiste à utiliser des assertions.


Donc, puisque nous faisons des tests de logiciels, nous devons cesser de placer des instructions assert dans le code de production, non? Permettez-moi de compter les façons dont cela est faux:

  1. Les assertions sont facultatives, donc si vous ne les aimez pas, exécutez simplement votre système avec les assertions désactivées.

  2. Les assertions vérifient les éléments que les tests ne peuvent pas (et ne devraient pas). Parce que les tests sont censés avoir une vue en boîte noire de votre système, tandis que les assertions ont une vue en boîte blanche. (Bien sûr, car ils y vivent.)

  3. Les assertions sont un excellent outil de documentation. Aucun commentaire n'a jamais été ni ne sera jamais aussi ambigu qu'un morceau de code affirmant la même chose. De plus, la documentation a tendance à devenir obsolète à mesure que le code évolue, et elle n'est en aucun cas applicable par le compilateur.

  4. Les assertions peuvent détecter des erreurs dans le code de test. Avez-vous déjà rencontré une situation où un test échoue et vous ne savez pas qui a tort - le code de production ou le test?

  5. Les assertions peuvent être plus pertinentes que les tests. Les tests vérifieront ce qui est prescrit par les exigences fonctionnelles, mais le code doit souvent faire certaines hypothèses qui sont beaucoup plus techniques que cela. Les personnes qui écrivent des documents d'exigences fonctionnelles pensent rarement à une division par zéro.

  6. Les assertions identifient les erreurs que les tests ne suggèrent que largement. Ainsi, votre test met en place des conditions préalables étendues, appelle un long morceau de code, rassemble les résultats et constate qu'ils ne sont pas comme prévu. Avec suffisamment de dépannage, vous finirez par trouver exactement où les choses ont mal tourné, mais les assertions le trouveront généralement en premier.

  7. Les assertions réduisent la complexité du programme. Chaque ligne de code que vous écrivez augmente la complexité du programme. Les assertions et le mot clé final (readonly) sont les deux seules constructions que je connaisse qui réduisent réellement la complexité du programme. C'est inestimable.

  8. Les assertions aident le compilateur à mieux comprendre votre code. Veuillez essayer ceci à la maison: void foo( Object x ) { assert x != null; if( x == null ) { } } votre compilateur devrait émettre un avertissement vous indiquant que la condition x == null est toujours faux. Cela peut être très utile.

Ce qui précède était un résumé d'un article de mon blog, 2014-09-21 "Assertions and Testing"

9
Mike Nakis

Je crois que la plupart des réponses manquent d'une distinction critique: cela dépend de la façon dont votre code sera utilisé.

Le module en question va-t-il être utilisé par d'autres clients indépendamment de l'application que vous testez? Si vous fournissez une bibliothèque ou une API pour une utilisation par des tiers, vous n'avez aucun moyen de vous assurer qu'ils appellent uniquement votre code avec une entrée valide. Vous devez valider toutes les entrées.

Mais si le module en question n'est utilisé que par du code que vous contrôlez, votre ami peut avoir un point. Vous pouvez utiliser des tests unitaires pour vérifier que le module en question n'est appelé qu'avec une entrée valide. Les vérifications des conditions préalables pourraient toujours être considérées comme une bonne pratique, mais c'est un compromis: si vous jetez le code qui vérifie les conditions que vous savez ne pouvez jamais rencontrer, cela obscurcit simplement l'intention du code.

Je ne suis pas d'accord pour dire que les vérifications des conditions préalables nécessitent davantage de tests unitaires. Si vous décidez que vous n'avez pas besoin de tester certaines formes d'entrées invalides, cela ne devrait pas avoir d'importance si la fonction contient des vérifications de précondition ou non. N'oubliez pas que les tests doivent vérifier le comportement et non les détails de l'implémentation.

5
JacquesB

Cet argument me déroute en quelque sorte, car lorsque j'ai commencé à pratiquer le TDD, mes tests unitaires de la forme "objet répond <d'une certaine manière> lorsque <entrée invalide>" ont augmenté de 2 ou 3 fois. Je me demande comment votre collègue réussit à réussir ce genre de tests unitaires sans que ses fonctions fassent la validation.

Le cas inverse, que les tests unitaires montrent que vous ne produisez jamais de mauvais sorties qui seront passés aux arguments d'autres fonctions, est beaucoup plus difficile à prouver. Comme dans le premier cas, cela dépend fortement de la couverture complète des cas Edge, mais vous avez la condition supplémentaire que toutes vos entrées de fonction doivent provenir des sorties d'autres fonctions dont vous avez testé les sorties et non, par exemple, des entrées utilisateur ou modules tiers.

En d'autres termes, ce que fait TDD ne vous empêche pas d'avoir besoin de code de validation autant que de vous éviter oublier de le faire.

3
Karl Bielefeldt

Je pense que j'interprète les remarques de votre collègue différemment de la plupart des autres réponses.

Il me semble que l'argument est:

  • Tout notre code est testé à l'unité.
  • Tout le code qui utilise votre composant est notre code, ou sinon, il est testé par quelqu'un d'autre (non explicitement indiqué, mais c'est ce que je comprends des "tests unitaires devraient détecter toute utilisation incorrecte de la classe").
  • Par conséquent, pour chaque appelant de votre fonction, il existe un test unitaire quelque part qui se moque de votre composant, et le test échoue si l'appelant transmet une valeur non valide à cette simulation.
  • Par conséquent, peu importe ce que fait votre fonction lorsqu'elle reçoit une valeur non valide, car nos tests indiquent que cela ne peut pas se produire.

Pour moi, cet argument a une certaine logique, mais accorde trop d'importance aux tests unitaires pour couvrir toutes les situations possibles. Le simple fait est que la couverture à 100% de ligne/branche/chemin n'exerce pas nécessairement tous les valeur que l'appelant pourrait passer, alors que la couverture à 100% de tous les états possibles de l'appelant (c'est-à-dire , toutes les valeurs possibles de ses entrées et variables) est impossible à calculer.

Par conséquent, j'aurais tendance à préférer les tests unitaires des appelants pour m'assurer que (pour autant que les tests se déroulent) ils ne passent jamais de mauvaises valeurs, et en plus pour exiger que votre composant échoue d'une manière reconnaissable lorsque une mauvaise valeur est transmise (au moins autant qu'il est possible de reconnaître les mauvaises valeurs dans la langue de votre choix). Cela facilitera le débogage lorsque des problèmes surviennent dans les tests d'intégration, et aidera également tous les utilisateurs de votre classe qui sont moins rigoureux à isoler leur unité de code de cette dépendance.

Sachez cependant que si vous documentez et testez le comportement de votre fonction lorsqu'une valeur <= 0 est transmise, alors les valeurs négatives ne sont plus invalides (au moins, pas plus invalide que n'importe quelle autre) du tout à throw, car cela aussi est documenté pour lever une exception!). Les appelants ont le droit de se fier à ce comportement défensif. Si le langage le permet, il se peut que ce soit dans tous les cas le meilleur scénario - la fonction a pas de "entrées invalides", mais les appelants qui s'attendent à ne pas provoquer la fonction en lançant une exception devraient être unitaires- suffisamment testés pour s’assurer qu’ils ne transmettent pas de valeurs qui provoquent cela.

Bien que votre collègue se soit trompé un peu moins que la plupart des réponses, j'arrive à la même conclusion, à savoir que les deux techniques se complètent. Programmez défensivement, documentez vos contrôles défensifs et testez-les. Le travail n'est "inutile" que si les utilisateurs de votre code ne peuvent pas bénéficier de messages d'erreur utiles lorsqu'ils font des erreurs. En théorie, s'ils testent à fond l'ensemble de leur code avant de l'intégrer au vôtre, et qu'il n'y a jamais d'erreur dans leurs tests, ils ne verront jamais les messages d'erreur. En pratique, même s'ils effectuent le TDD et l'injection de dépendance totale, ils peuvent toujours explorer pendant le développement ou il peut y avoir une interruption dans leurs tests. Le résultat est qu'ils appellent votre code avant que leur code ne soit parfait!

2
Steve Jessop

Un bon ensemble de tests exercera l'interface externe de votre classe et garantira que de tels abus génèrent la réponse correcte (une exception, ou tout ce que vous définissez comme "correct"). En fait, le premier cas de test que j'écris pour une classe est d'appeler son constructeur avec des arguments hors limites.

Le type de programmation défensive qui a tendance à être éliminé par une approche entièrement testée est la validation inutile des invariants internes qui ne peuvent pas être violés par du code externe.

Une idée utile que j'emploie parfois est de fournir une méthode qui teste les invariants de l'objet; votre méthode de démontage peut l'appeler pour valider que vos actions externes sur l'objet ne cassent jamais les invariants.

1
Toby Speight

Les tests définissent le contrat de votre classe.

En corollaire, l'absence d'un test définit un contrat qui comprend comportement indéfini . Ainsi, lorsque vous passez null à Foo::Frobnicate(Widget widget), et que des ravages indicibles s'ensuivent, vous êtes toujours dans le contrat de votre classe.

Plus tard, vous décidez, "nous ne voulons pas la possibilité d'un comportement indéfini", ce qui est un choix judicieux. Cela signifie que vous devez avoir un comportement attendu pour passer null à Foo::Frobnicate(Widget widget).

Et vous documentez cette décision en incluant un

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

La défense contre les abus est une fonctionnalité , développée en raison d'une exigence. (Toutes les interfaces ne nécessitent pas de contrôles rigoureux contre les utilisations abusives; par exemple, celles qui sont utilisées de manière très étroite.)

La fonctionnalité nécessite des tests: la défense contre les abus fonctionne-t-elle réellement? Le but de tester cette fonctionnalité est d'essayer de montrer qu'elle ne l'est pas: de créer une mauvaise utilisation du module qui n'est pas prise en compte par ses vérifications.

Si des contrôles spécifiques sont une fonctionnalité requise, il est en effet absurde d'affirmer que l'existence de certains tests les rend inutiles. Si c'est une caractéristique d'une fonction qui (disons) lève une exception lorsque le paramètre trois est négatif, alors ce n'est pas négociable; il le fera.

Cependant, je soupçonne que votre collègue a du sens du point de vue d'une situation dans laquelle il n'y a pas d'obligation de spécifique contrôles sur les entrées, avec des réponses spécifiques aux mauvaises entrées: une situation dans laquelle il n'est qu'une exigence générale comprise pour la robustesse.

Les contrôles à l'entrée dans une fonction de niveau supérieur sont là, en partie, pour protéger un code interne faible ou mal testé contre des combinaisons inattendues de paramètres (tels que si le code est bien testé, les contrôles ne sont pas nécessaires: le code peut simplement " météo "les mauvais paramètres).

Il y a du vrai dans l'idée du collègue, et ce qu'il veut probablement dire: si nous construisons une fonction à partir de pièces de niveau inférieur très robustes qui sont codées de manière défensive et testées individuellement contre toute utilisation abusive, alors il est possible que la fonction de niveau supérieur soit robuste sans avoir ses propres auto-contrôles étendus.

Si son contrat est violé, cela se traduira par une mauvaise utilisation des fonctions de niveau inférieur, peut-être en levant des exceptions ou autre chose.

Le seul problème avec cela est que les exceptions de niveau inférieur ne sont pas spécifiques à l'interface de niveau supérieur. Que ce soit un problème dépend des exigences. Si l'exigence est simplement "la fonction doit être robuste contre les abus et lever une sorte d'exception plutôt que de planter, ou continuer à calculer avec des données d'ordures" alors en fait, elle pourrait être couverte par toute la robustesse des pièces de niveau inférieur sur lesquelles elle est construit.

Si la fonction nécessite un rapport d'erreurs très spécifique et détaillé lié à ses paramètres, les vérifications de niveau inférieur ne satisfont pas entièrement à ces exigences. Ils s'assurent seulement que la fonction explose d'une manière ou d'une autre (ne continue pas avec une mauvaise combinaison de paramètres, produisant un résultat incorrect). Si le code client est écrit pour intercepter spécifiquement certaines erreurs et les gérer, il peut ne pas fonctionner correctement. Le code client peut lui-même obtenir, en entrée, les données sur lesquelles les paramètres sont basés, et il peut s'attendre à ce que la fonction les vérifie et traduise les mauvaises valeurs en erreurs spécifiques telles que documentées (afin de pouvoir les gérer). correctement) plutôt que certaines autres erreurs qui ne sont pas traitées et peut-être arrêter l'image du logiciel.

TL; DR: votre collègue n'est probablement pas un idiot; vous vous contentez de vous parler les uns des autres avec des perspectives différentes autour de la même chose, car les exigences ne sont pas entièrement définies et chacun de vous a une idée différente de ce que sont les "exigences non écrites". Vous pensez que lorsqu'il n'y a pas d'exigences spécifiques concernant la vérification des paramètres, vous devez quand même coder la vérification détaillée; pense le collègue, laissez simplement exploser le code de niveau inférieur robuste lorsque les paramètres sont incorrects. Il est quelque peu improductif de discuter des exigences non écrites par le biais du code: reconnaissez que vous n'êtes pas d'accord sur les exigences plutôt que sur le code. Votre façon de coder reflète ce que vous pensez que les exigences sont; la manière du collègue représente sa vision des exigences. Si vous le voyez de cette façon, il est clair que ce qui est bien ou mal n'est pas dans le code lui-même; le code est juste un proxy pour votre avis sur ce que devrait être la spécification.

1
Kaz

Les interfaces publiques peuvent être et seront mal utilisées

L'affirmation de votre collègue "les tests unitaires devraient détecter toute utilisation incorrecte de la classe" est strictement fausse pour toute interface qui n'est pas privée. Si une fonction publique peut être appelée avec des arguments entiers, elle peut et sera appelée avec des arguments entiers any, et le code doit se comporter de manière appropriée. Si une signature de fonction publique accepte par ex. Java Double type, puis null, NaN, MAX_VALUE, -Inf sont toutes des valeurs possibles. Vos tests unitaires ne peuvent pas détecter les utilisations incorrectes de la classe car ces tests ne peuvent pas tester le code qui utilisera cette classe, parce que ce code n'est pas encore écrit, peut-être pas écrit par vous, et sera certainement en dehors de la portée des tests unitaires votre.

D'un autre côté, cette approche peut être valable pour les propriétés privées (espérons-le beaucoup plus nombreuses) - si une classe peut assurer qu'un fait est toujours vrai (par exemple, la propriété X ne peut jamais être nulle, position entière ne dépasse pas la longueur maximale, lorsque la fonction A est appelée, toutes les structures de données prérequises sont bien formées), il peut être approprié d'éviter de le vérifier encore et encore pour des raisons de performances, et de s'appuyer à la place sur des tests unitaires.

1
Peteris

Les tests de TDD vont détecter les erreurs lors de le développement du code.

Les vérifications des limites que vous décrivez dans le cadre de la programmation défensive captureront des erreurs lors de l'utilisation du code.

Si les deux domaines sont identiques, c'est-à-dire que le code que vous écrivez n'est utilisé qu'en interne par ce projet spécifique, alors il peut être vrai que TDD évitera la nécessité de vérifier les limites de la programmation défensive que vous décrivez, mais seulement si - ces types de vérification des limites sont spécifiquement effectués dans les tests TDD.


À titre d'exemple spécifique, supposons qu'une bibliothèque de code financier ait été développée à l'aide de TDD. L'un des tests pourrait affirmer qu'une valeur particulière ne peut jamais être négative. Cela garantit que les développeurs de la bibliothèque n'utilisent pas accidentellement les classes lors de l'implémentation des fonctionnalités.

Mais après la sortie de la bibliothèque et que je l'utilise dans mon propre programme, ces tests TDD ne m'empêchent pas d'attribuer une valeur négative (en supposant qu'elle soit exposée). La vérification des limites le ferait.

Mon point est que même si une assertion TDD pourrait résoudre le problème de la valeur négative si le code n'est utilisé qu'en interne dans le cadre du développement d'une application plus grande (sous TDD), s'il s'agit d'une bibliothèque utilisée par d'autres programmeurs sans le framework et les tests TDD, vérification des limites.

0
Blackhawk

TDD et programmation défensive vont de pair. L'utilisation des deux n'est pas redondante, mais en fait complémentaire. Lorsque vous avez une fonction, vous voulez vous assurer qu'elle fonctionne comme décrit et écrire des tests pour elle; si vous ne couvrez pas ce qui se passe lorsque dans le cas d'une mauvaise entrée, d'un mauvais retour, d'un mauvais état, etc., vous n'écrivez pas suffisamment vos tests, et votre code sera fragile même si tous vos tests réussissent.

En tant qu'ingénieur embarqué, j'aime utiliser l'exemple d'écriture d'une fonction pour simplement ajouter deux octets ensemble et retourner le résultat comme ceci:

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

Maintenant, si vous venez de faire simplement *(sum) = a + b cela fonctionnerait, mais seulement avec certains entrées. a = 1 Et b = 2 Feraient sum = 3; cependant, comme la taille de la somme est un octet, a = 100 et b = 200 feraient sum = 44 en raison d'un débordement. En C, vous renverriez une erreur dans ce cas pour signifier que la fonction a échoué; lever une exception est la même chose dans votre code. Ne pas considérer les échecs ou tester comment les gérer ne fonctionnera pas à long terme, car si ces conditions se produisent, elles ne seront pas gérées et pourraient causer un certain nombre de problèmes.

0
Dom