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 " une classe doit s'assurer que ses objets ont un état valide lorsqu'elles 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 interagit de manière illégale. A 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 un 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. D'autre part, mon collègue estime qu'une telle vérification est redondante, car les tests unitaires devraient détecter toute utilisation incorrecte de la classe. De plus, il pense que les validations de programmation défensive doivent également être testées à l'unité. La programmation défensive ajoute donc beaucoup de travail et n'est donc pas optimale pour le TDD.
Est-il vrai que TDD est capable de remplacer la programmation défensive? La validation des paramètres (et je ne parle pas de saisie utilisateur) est-elle inutile? Ou les deux techniques se complètent-elles?
la source
Réponses:
C'est ridicule. TDD oblige le code à réussir les tests et oblige tout le code à en effectuer quelques tests. Cela n'empêche pas vos consommateurs d'appeler le code de manière incorrecte, pas plus qu'il n'empêche par magie les programmeurs de manquer des cas de test.
Aucune méthodologie ne peut obliger les utilisateurs à utiliser le code correctement.
Il y a un léger argument à faire valoir que si vous maîtrisiez parfaitement TDD, vous auriez capturé votre contrôle> 0 dans un scénario de test, avant de le mettre en œuvre, et vous avez résolu ce problème - probablement en ajoutant le chèque. Mais si vous utilisiez TDD, votre exigence (> 0 dans le constructeur) apparaîtrait tout d' abord comme un test qui échoue. Ainsi, vous donnant le test après avoir ajouté votre chèque.
Il est également raisonnable de tester certaines conditions défensives (vous avez ajouté une logique, pourquoi ne voudriez-vous pas tester quelque chose d'aussi facilement testable?). Je ne sais pas pourquoi vous semblez être en désaccord avec cela.
TDD développera les tests. La mise en œuvre de la validation des paramètres les fera passer.
la source
La programmation défensive et les tests unitaires sont deux manières différentes de détecter les erreurs et ont chacun des forces différentes. L'utilisation d'un seul moyen de détecter les erreurs rend vos mécanismes de détection d'erreur fragiles. L'utilisation des deux permet de récupérer les erreurs qui auraient pu être omises par l'un ou l'autre, même dans du code autre qu'une API publique; Par exemple, il est possible que quelqu'un ait 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 d’attraper l’erreur.
Dans la sécurité de l'information, cela s'appelle Défense en profondeur. Le fait de disposer de plusieurs niveaux de défense garantit que si l’un échoue, il en reste d’autres pour le rattraper.
Votre collègue a raison sur un point: vous devriez tester vos validations, mais ce n'est pas du "travail inutile". C'est la même chose que de tester tout autre code, vous voulez vous assurer que toutes les utilisations, même celles qui ne sont pas valides, ont un résultat attendu.
la source
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 écrire d'abord un test - suivez le cycle rouge-vert-refactorisation avec religion. Cela signifie que si vous souhaitez ajouter une validation, commencez par écrire un test nécessitant cette validation. Appelez la méthode en question avec des nombres négatifs et avec zéro et attendez-elle à une exception.
De plus, n'oubliez pas l'étape du «refactor». Bien que TDD soit piloté par les tests , cela ne signifie pas uniquement des tests . Vous devez toujours appliquer une conception appropriée et écrire du code sensible. L'écriture de code défensif est un code sensible, car il rend les attentes plus explicites et votre code globalement plus robuste - repérer tôt les erreurs possibles facilite leur débogage.
Mais ne sommes-nous pas supposés utiliser des tests pour localiser les erreurs? Les assertions et les tests sont complémentaires. Une bonne stratégie de test combinera différentes approches pour s'assurer que le logiciel est robuste. Seuls les tests unitaires ou les tests d'intégration ou les assertions du code ne sont pas satisfaisants. Vous avez besoin d'une bonne combinaison pour obtenir un degré de confiance suffisant de votre logiciel avec un effort acceptable.
Ensuite, il y a un très grand malentendu conceptuel de votre collègue: les tests unitaires ne peuvent jamais tester les utilisations de votre classe, mais 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 divers composants fonctionnait, mais l’explosion combinatoire de scénarios de test possibles rend impossible tout test. Les tests d'intégration doivent donc se limiter à quelques cas importants. Des tests plus détaillés couvrant également les cas extrêmes et les cas d'erreur conviennent mieux aux tests unitaires.
la source
Les tests sont là pour soutenir et assurer la programmation défensive
La programmation défensive protège l'intégrité du système au moment 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 les échafaudages utilisés pour ériger un haut mur de briques ou un dôme en pierre. Vous ne laissez pas de pièces importantes en dehors de la structure, car vous avez un échafaudage qui la retient pendant la construction. Vous avez un échafaudage qui le tient en place 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, voire nuisibles. Par exemple, si vous mettez la connaissance intrinsèque sur le code dans les commentaires , puis modifiez le code, les commentaires deviennent au mieux inutiles et au pire nuisibles.
Donc, supposons que vous mettiez beaucoup de connaissances intrinsèques de votre base de code dans les tests, comme MethodA ne peut pas prendre une valeur nulle et l'argument de MethodB doit être
> 0
. Ensuite, le code change. Null est acceptable pour A maintenant, et B peut prendre des valeurs aussi petites que -10. Les tests existants sont maintenant fonctionnellement faux, mais continueront à réussir.Oui, vous devriez mettre à jour les tests en même temps que le code. Vous devez également mettre à jour (ou supprimer) les commentaires en même temps que 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 et non aux tests.
Qu'est ce qui pourrait aller mal?
L’objectif des tests est de penser à tout ce qui pourrait mal tourner, d’écrire un test qui vérifie le bon comportement, puis de créer le code d’exécution de sorte 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, davantage de tests sont écrits pour modéliser les conditions qui manifestent le bogue. Ensuite, le code est corrigé, avec le code nécessaire à la réussite de ces tests, et les nouveaux tests restent dans la suite de tests.
Un bon ensemble de tests va passer les bons et les mauvais arguments à une fonction / méthode et attendre des résultats cohérents. Cela signifie à son tour que le composant testé utilisera des contrôles de précondition (programmation défensive) pour confirmer les arguments qui lui sont transmis.
Généralement parlant ...
Par exemple, si un argument null dans une procédure particulière est invalide, alors au moins un test passera un null et attendra une exception / erreur "argument non valide".
Au moins un autre test va bien sûr passer un argument valide - ou parcourir un grand tableau et transmettre de nombreux arguments valides - et confirmer que l'état résultant est approprié.
Si un test ne réussit pas cet argument null et est mis en attente avec l'exception attendue (et que cette exception a été levée parce que le code a vérifié l'état de manière défensive), la valeur null peut être affectée à une propriété d'une classe ou être enterrée. dans une collection de quelque sorte où il ne devrait pas être.
Cela peut entraîner un comportement inattendu dans une partie du système totalement différente à laquelle l'instance de classe est transmise, dans des paramètres régionaux éloignés après la livraison du logiciel . Et c'est le genre de chose que nous essayons d'éviter, n'est-ce pas?
Cela pourrait même être pire. L'instance de classe avec l'état non valide pourrait être sérialisée et stockée, uniquement pour provoquer un échec lorsqu'elle sera 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 bien l'instance de classe peut être sérialisée et transmise à un système totalement différent créé par une autre entité et ce système peut tomber en panne.
Surtout si les programmeurs de cet autre système ne codaient pas de manière défensive.
la source
Au lieu de TDD, parlons de "test de logiciel" en général, et de "programmation défensive" en général, parlons de ma manière préférée de faire de la programmation défensive, qui consiste à utiliser des assertions.
Donc, puisque nous testons des logiciels, nous devrions cesser de placer des déclarations d’assertion dans le code de production, non? Laissez-moi compter les façons dont cela est faux:
Les assertions sont facultatives. Si vous ne les aimez pas, exécutez simplement votre système avec les assertions désactivées.
Les assertions vérifient ce que les tests ne peuvent pas (et ne devraient pas faire). Parce que les tests sont supposés avoir une vue en boîte noire de votre système, alors que les assertions ont une vue en boîte blanche. (Bien sûr, puisqu'ils y vivent.)
Les assertions sont un excellent outil de documentation. Aucun commentaire n'a jamais été et ne sera jamais aussi clair qu'un code affirmant la même chose. De plus, la documentation tend à devenir obsolète à mesure que le code évolue, et le compilateur ne peut en aucune manière la mettre en application.
Les assertions peuvent intercepter des erreurs dans le code de test. Avez-vous déjà rencontré une situation dans laquelle un test échoue et vous ne savez pas qui a tort - le code de production ou le test?
Les assertions peuvent être plus pertinentes que les tests. Les tests vérifient ce qui est prescrit par les exigences fonctionnelles, mais le code doit souvent faire certaines hypothèses beaucoup plus techniques que cela. Les personnes qui rédigent des documents d’exigences fonctionnelles pensent rarement à une division par zéro.
Les assertions identifient les erreurs auxquelles on ne fait que tester de manière générale. Ainsi, votre test définit des conditions préalables étendues, invoque un long morceau de code, rassemble les résultats et constate qu’ils ne sont pas conformes à vos attentes. Si vous avez suffisamment de problèmes, vous finirez par trouver exactement où les choses se sont mal passées, mais les affirmations le trouveront d’abord.
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.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 conditionx == null
est toujours fausse. Cela peut être très utile.Ce qui précède est un résumé d'un billet de mon blog du 2014-09-21 "Assertions and Testing"
la source
Je crois que la plupart des réponses manquent à une distinction essentielle: cela dépend de la façon dont votre code va être utilisé.
Le module en question sera-t-il utilisé par d'autres clients indépendamment de l'application que vous testez? Si vous fournissez une bibliothèque ou une API à l'usage de tiers, vous ne pouvez vous assurer qu'ils n'appellent votre code qu'avec une entrée valide. Vous devez valider toutes les entrées.
Mais si le module en question n’est utilisé que par le code que vous contrôlez, alors votre ami aura peut-être un sens. Vous pouvez utiliser des tests unitaires pour vérifier que le module en question est appelé uniquement avec une entrée valide. Les vérifications de précondition peuvent toujours être considérées comme une bonne pratique, mais c'est un compromis: si vous écrasez le code qui vérifie si une condition ne peut jamais survenir, elle masque simplement l'intention du code.
Je ne suis pas d'accord sur le fait que les contrôles 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 non valides, le fait que la fonction contienne ou non des vérifications préalables n'a aucune importance. N'oubliez pas que les tests doivent vérifier le comportement, pas les détails de la mise en œuvre.
la source
Cet argument me laisse perplexe, car lorsque j'ai commencé à utiliser TDD, mes tests unitaires de la forme "objet répond <certaine manière> lorsque <entrée non valide>" ont été multipliés par 2 ou 3. Je me demande comment votre collègue réussit à réussir ce type de tests unitaires sans que ses fonctions fassent la validation.
Le cas contraire, selon lequel les tests unitaires montrent que vous ne produisez jamais de mauvais résultats qui seront transmis aux arguments des autres fonctions, est beaucoup plus difficile à prouver. Comme dans le premier cas, cela dépend beaucoup de la couverture complète des cas extrêmes, mais vous devez également vous assurer que toutes les entrées de vos fonctions doivent provenir des sorties des autres fonctions dont vous avez testé les sorties et non pas, par exemple, des entrées utilisateur. modules tiers.
En d'autres termes, ce que fait TDD ne vous empêche pas d' avoir besoin d'un code de validation mais vous évite de l' oublier .
la source
Je pense que j'interprète les propos de votre collègue différemment de la plupart des réponses restantes.
Il me semble que l'argument est le suivant:
Pour moi, cet argument a une certaine logique, mais fait trop confiance aux tests unitaires pour couvrir toutes les situations possibles. Le fait est qu'une couverture à 100% en lignes / branches / chemins n'exerce pas nécessairement toutes les valeurs que l'appelant peut transmettre, alors qu'une 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 , je tendance à préférer à l' unité-test , les appelants pour faire en sorte que (pour autant que les essais vont) ils ne passent jamais dans de mauvaises valeurs, et en plus d'exiger que votre composant ne d'une manière reconnaissable quand une mauvaise valeur est passée dans ( au moins dans la mesure où il est possible de reconnaître les mauvaises valeurs dans la langue de votre choix). Cela facilitera le débogage en cas de problèmes lors des tests d'intégration et aidera également les utilisateurs de votre classe qui ne sont pas 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, les valeurs négatives ne sont plus invalides (du moins, pas plus que n'importe quel argument
throw
, car aussi est documenté pour jeter une exception!). Les appelants ont le droit de se fier à ce comportement défensif. Si la langue le permet, il se peut que ce soit de toute façon le meilleur scénario. La fonction ne comporte pas "d'entrées non valides", mais les appelants qui s'attendent à ne pas provoquer la fonction en lançant une exception doivent être suffisamment testés pour garantir leur suppression. t transmettre les valeurs qui causent cela.Bien que je pense que votre collègue a un peu moins tort que la plupart des réponses, je parviens à la même conclusion, à savoir que les deux techniques se complètent. Programmez de manière défensive, documentez vos vérifications défensives 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 commettent des erreurs. En théorie, s'ils testent minutieusement tous leurs codes avant de les intégrer au vôtre, et que leurs tests ne font jamais d'erreur, ils ne verront jamais les messages d'erreur. En pratique, même s'ils effectuent une injection de dépendance totale et de TDD, ils peuvent quand même explorer pendant le développement ou les tests peuvent être échoués. Le résultat est qu'ils appellent votre code avant que leur code soit parfait!
la source
Les interfaces publiques peuvent et seront mal utilisées
La réclamation de votre collègue "les tests unitaires doivent détecter toute utilisation incorrecte de la classe" est strictement fausse pour toute interface non privée. Si une fonction publique peut être appelée avec des arguments entiers, elle peut et sera appelée avec tous les arguments entiers, et le code doit se comporter de manière appropriée. Si une signature de fonction publique accepte, par exemple, le type Java Double, les valeurs null, NaN, MAX_VALUE, -Inf sont toutes 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, car ce code n'est pas encore écrit, il se peut qu'il ne soit pas écrit par vous et sortira définitivement du cadre de vos tests unitaires. .
D'autre part, cette approche peut être valable pour les propriétés privées (beaucoup plus nombreuses, espérons-le) - si une classe peut s'assurer que certains faits sont toujours vrais (par exemple, la propriété X ne peut jamais être nulle, la 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 alors être judicieux d’éviter de le vérifier encore et encore pour des raisons de performances et de faire appel à des tests unitaires.
la source
La défense contre les utilisations abusives est une fonctionnalité , développée pour répondre à une nécessité. (Toutes les interfaces ne nécessitent pas de contrôles rigoureux contre les utilisations abusives; par exemple, les interfaces internes très étroitement utilisées.)
La fonctionnalité nécessite des tests: la défense contre les utilisations abusives fonctionne-t-elle réellement? Le but de tester cette fonctionnalité est d’essayer de montrer que ce n’est pas le cas: il faut remédier à une mauvaise utilisation du module qui n’est pas détectée par ses vérifications.
Si des vérifications spécifiques sont une fonctionnalité requise, il est en effet absurde d'affirmer que l'existence de certains tests les rend inutiles. S'il s'agit d'une caractéristique d'une fonction qui (par exemple) génère une exception lorsque le paramètre trois est négatif, cela n'est pas négociable; il doit faire ça.
Toutefois, j’imagine que votre collègue a tout à fait du sens du point de vue d’une situation dans laquelle il n’est pas nécessaire de procéder à un contrôle spécifique des intrants, avec des réponses spécifiques aux mauvais intrants: une situation dans laquelle il n’existe qu’une exigence générale bien comprise. robustesse.
Les contrôles lors de l’entrée dans certaines fonctions de niveau supérieur servent en partie à protéger un code interne faible ou mal testé contre des combinaisons inattendues de paramètres (de sorte 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).
L’idée du collègue est véridique et ce qu’il entend probablement par là est la suivante: si nous construisons une fonction à partir d’éléments très robustes de niveau inférieur codés de manière défensive et testés individuellement contre tout abus, il est alors possible que la fonction de niveau supérieur soit robuste sans avoir ses propres autocontrôles approfondis.
Si son contrat est violé, il se traduira par une utilisation abusive des fonctions de niveau inférieur, peut-être en lançant des exceptions ou autre.
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 ou non dépend des exigences. Si l'exigence est simplement "la fonction doit être robuste contre les utilisations abusives et émettre une sorte d'exception plutôt que de planter ou de continuer à calculer avec des données erronées", elle pourrait en fait être couverte par toute la robustesse des éléments de niveau inférieur sur lesquels elle repose. construit.
Si la fonction demande des rapports d'erreur très spécifiques et détaillés relatifs à ses paramètres, les contrôles de niveau inférieur ne répondent pas pleinement à ces exigences. Ils veillent uniquement à ce que la fonction explose (ne continue pas avec une mauvaise combinaison de paramètres, ce qui produira un résultat incohérent). Si le code client est écrit pour intercepter certaines erreurs et les gérer, il est possible que cela ne fonctionne pas 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 valeurs incorrectes en erreurs spécifiques comme documenté (afin qu’elle puisse les gérer). erreurs correctement) plutôt que d’autres erreurs qui ne sont pas traitées et qui risquent d’arrêter l’image logicielle.
TL; DR: votre collègue n'est probablement pas un idiot; vous vous contentez de parler les uns des autres avec des points de vue différents sur la même chose, car les exigences ne sont pas complètement 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 en matière de vérification des paramètres, vous devez néanmoins coder une vérification détaillée; le collègue pense, laissez simplement le code de niveau inférieur robuste exploser lorsque les paramètres sont erronés. Il est quelque peu improductif de discuter des exigences non écrites via le 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 être les exigences; la façon dont le collègue représente sa vision des exigences. Si vous le voyez ainsi, il est clair que ce qui est juste ou faux n’est pas t dans le code lui-même; le code est juste un proxy pour votre opinion de ce que la spécification devrait être.
la source
Les tests définissent le contrat de votre classe.
Corollairement, l' absence de test définit un contrat qui inclut un comportement indéfini . Ainsi, lorsque vous passez
null
àFoo::Frobnicate(Widget widget)
, et que des ravages incalculables au moment de l'exécution 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 pouvoir passer
null
àFoo::Frobnicate(Widget widget)
.Et vous documentez cette décision en incluant un
la source
Une bonne série de tests permettra d’exercer l’ interface externe de votre classe et de s’assurer que de telles utilisations abusives 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 consiste à appeler son constructeur avec des arguments hors de portée.
Le type de programmation défensive qui tend à être éliminé par une approche entièrement testée est la validation inutile d' invariants internes qui ne peuvent être violés par du code externe.
Une idée utile que j’utilise 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.
la source
Les tests de TDD détecteront les erreurs lors du développement du code .
La vérification des limites que vous décrivez dans le cadre de la programmation défensive détectera les 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é en interne que par ce projet spécifique, il est alors possible que TDD exclue la nécessité de la vérification des limites de la programmation défensive que vous décrivez, mais uniquement si ces types de vérification des limites sont spécifiquement effectués dans les tests TDD .
A titre d'exemple spécifique, supposons qu'une bibliothèque de codes financiers ait été développée à l'aide de TDD. L'un des tests peut affirmer qu'une valeur donnée ne peut jamais être négative. Cela garantit que les développeurs de la bibliothèque n'abusent pas des classes par inadvertance lorsqu'ils implémentent les fonctionnalités.
Mais une fois la bibliothèque publiée et utilisée 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 serait.
Mon argument est qu’une assertion TDD pourrait résoudre le problème de la valeur négative si le code n’est utilisé que de manière 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 TDD. cadre et tests , vérification des limites.
la source
TDD et la 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 que celle-ci fonctionne comme décrit et écrire des tests pour elle. si vous ne couvrez pas ce qui se passe dans le cas d'une mauvaise entrée, d'un mauvais retour, d'un mauvais état, etc., vous n'écrivez pas vos tests de manière suffisamment robuste et votre code sera fragile même si tous vos tests réussissent.
En tant qu'ingénieur intégré, j'aime bien utiliser l'exemple de l'écriture d'une fonction pour simplement ajouter deux octets ensemble et renvoyer le résultat comme suit:
Maintenant, si vous le faisiez simplement,
*(sum) = a + b
cela fonctionnerait, mais seulement avec quelques intrants.a = 1
etb = 2
feraitsum = 3
; Cependant, parce que la taille de la somme est un octet,a = 100
etb = 200
seraitsum = 44
due à un dépassement de capacité. En C, vous renverriez une erreur dans ce cas pour indiquer que la fonction a échoué; Lancer une exception est la même chose dans votre code. Ne pas prendre en compte 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 grand nombre de problèmes.la source
sum
est un pointeur nul?).