J'écrivais récemment un petit morceau de code qui indiquerait de manière conviviale l'âge d'un événement. Par exemple, cela pourrait indiquer que l'événement s'est produit «Il y a trois semaines» ou «Il y a un mois» ou «Hier».
Les exigences étaient relativement claires et constituaient un exemple parfait pour un développement piloté par des tests. J'ai écrit les tests un par un, en implémentant le code pour réussir chaque test, et tout semblait parfaitement fonctionner. Jusqu'à ce qu'un bogue apparaisse dans la production.
Voici le morceau de code pertinent:
now = datetime.datetime.utcnow()
today = now.date()
if event_date.date() == today:
return "Today"
yesterday = today - datetime.timedelta(1)
if event_date.date() == yesterday:
return "Yesterday"
delta = (now - event_date).days
if delta < 7:
return _number_to_text(delta) + " days ago"
if delta < 30:
weeks = math.floor(delta / 7)
if weeks == 1:
return "A week ago"
return _number_to_text(weeks) + " weeks ago"
if delta < 365:
... # Handle months and years in similar manner.
Les tests vérifiaient le cas d'un événement survenu aujourd'hui, hier, il y a quatre jours, il y a deux semaines, il y a une semaine, etc., et le code a été construit en conséquence.
Ce qui m’a manqué, c’est qu’un événement peut se produire un jour avant hier, alors qu’il ya un jour: par exemple, un événement se produisant il y a vingt-six heures le serait il ya un jour, alors que ce n’est pas exactement hier si nous avons maintenant une heure du matin. quelque chose, mais puisque le delta
est un entier, ce ne sera qu'un. Dans ce cas, l'application affiche «Il y a un jour», ce qui est évidemment inattendu et non géré dans le code. Il peut être corrigé en ajoutant:
if delta == 1:
return "A day ago"
juste après le calcul du delta
.
La seule conséquence négative de ce bug est que j'ai perdu une demi-heure à me demander comment cette affaire pourrait se dérouler (en pensant qu'il s'agit de fuseaux horaires, malgré l'utilisation uniforme de l'UTC dans le code), mais sa présence me trouble. Il indique que:
- Il est très facile de commettre une erreur logique même dans un code source aussi simple.
- Le développement piloté par les tests n'a pas aidé.
Ce qui est également inquiétant, c'est que je ne vois pas comment on pourrait éviter de tels insectes. En plus de réfléchir avant d’écrire du code, la seule façon de penser est d’ajouter beaucoup d’affirmations pour les cas qui, à mon avis, ne se produiraient jamais (comme je le pensais il ya un jour, c’est nécessairement hier), puis de passer en revue chaque seconde ces dix dernières années, en recherchant toute violation d’affirmation, ce qui semble trop complexe.
Comment pourrais-je éviter de créer ce bogue en premier lieu?
la source
Réponses:
Ce sont les types d'erreur que vous trouvez généralement dans l' étape de refactor de red / green / refactor. N'oubliez pas cette étape! Considérons un refactor comme celui-ci (non testé):
Ici, vous avez créé 3 fonctions à un niveau d'abstraction inférieur qui sont beaucoup plus cohérentes et plus faciles à tester isolément. Si vous laissiez de côté un laps de temps que vous aviez prévu, il se présenterait comme un pouce endolori dans les fonctions d'assistance simplifiées. En outre, en supprimant la duplication, vous réduisez le risque d'erreur. Vous devrez en fait ajouter du code pour mettre en œuvre votre casse cassée.
D'autres cas de test plus subtils viennent également à l'esprit quand on regarde une forme refactorisée comme celle-ci. Par exemple, que
best_unit
faire sidelta
est négatif?En d’autres termes, la refactorisation n’est pas simplement destinée à la rendre jolie. Cela permet aux humains de repérer plus facilement les erreurs que le compilateur ne peut pas.
la source
pluralize
travailler uniquement pour un sous-ensemble de mots anglais sera un handicap.pluralize
utilisationnum
etunit
de construire une clé d'une sorte de tirer une chaîne de format provenant d' un fichier table / ressource. OU vous pourriez avoir besoin d'une réécriture complète de la logique, car vous avez besoin d'unités différentes ;-)On dirait que cela a aidé, c’est juste que vous n’avez pas eu de test pour le scénario "il ya un jour". Vous avez probablement ajouté un test après la découverte de ce cas; c'est toujours TDD, en ce que lorsque des bugs sont trouvés, vous écrivez un test unitaire pour détecter le bogue, puis corrigez-le.
Si vous oubliez d'écrire un test de comportement, TDD n'a rien pour vous aider. vous oubliez d'écrire le test et n'écrivez donc pas l'implémentation.
la source
datetime.utcnow()
retirer de la fonction et à le remplacernow
par un argument (reproductible).Les tests n’aideront pas beaucoup si un problème est mal défini. De toute évidence, vous mélangez des jours calendaires avec des jours calculés en heures. Si vous vous en tenez aux jours du calendrier, à 1 heure du matin, il y a 26 heures, ce n'est pas hier. Et si vous vous en tenez aux heures, il y a 26 heures, il y a 1 jour, quelle que soit l'heure.
la source
Tu ne peux pas. TDD vous protège des problèmes éventuels dont vous êtes au courant. Cela n'aide pas si vous rencontrez des problèmes que vous n'avez jamais envisagés. Votre meilleur choix est de demander à quelqu'un d'autre de tester le système, il peut trouver les cas extrêmes que vous n'avez jamais envisagés.
Lecture connexe: Est-il possible d'atteindre l'état de bogue zéro absolu pour les logiciels à grande échelle?
la source
Il y a deux approches que je prends normalement et que je trouve utiles.
Tout d'abord, je cherche les cas extrêmes. Ce sont des endroits où le comportement change. Dans votre cas, le comportement change à plusieurs moments de la séquence de jours entiers positifs. Il y a un cas limite à zéro, à une heure, à sept ans, etc. J'aurais des cas de test à -1 jours, 0 jours, 1 heure, 23 heures, 24 heures, 25 heures, 6 jours, 7 jours, 8 jours, etc.
La deuxième chose que je rechercherais, ce sont les comportements. Dans votre logique pendant des semaines, vous avez une manipulation spéciale pendant une semaine. Vous avez probablement une logique similaire dans chacun de vos autres intervalles non représentés. Cette logique n’est pas présente depuis des jours. J'examinerais cela avec suspicion jusqu'à ce que je puisse expliquer de manière vérifiable en quoi ce cas est différent ou ajouter la logique.
la source
Vous ne pouvez pas intercepter les erreurs logiques présentes dans vos exigences avec TDD. Mais encore, TDD aide. Après tout, vous avez trouvé l'erreur et ajouté un scénario de test. Mais fondamentalement, TDD assure uniquement que le code est conforme à votre modèle mental. Si votre modèle mental est défectueux, les cas de test ne les détecteront pas.
Mais gardez à l’esprit, tout en corrigeant le bogue, les cas de test que vous aviez déjà vérifiés s’assuraient qu'aucun comportement existant ne fonctionnait. C'est très important, il est facile de corriger un bogue mais d'en introduire un autre.
Afin de rechercher ces erreurs à l'avance, vous essayez généralement d'utiliser des scénarios de test basés sur la classe d'équivalence. En utilisant ce principe, vous choisiriez un cas de chaque classe d'équivalence, puis tous les cas extrêmes.
Vous choisiriez une date à partir d’aujourd’hui, d’hier, de quelques jours, d’une semaine exactement et de plusieurs semaines, comme exemples de chaque classe d’équivalence. Lors du test des dates, vous devez également vous assurer que vos tests n'utilisent pas la date système, mais utilisent une date prédéterminée pour la comparaison. Cela mettrait également en évidence certains cas extrêmes: vous vous assureriez de faire vos tests à une heure quelconque de la journée, vous le feriez avec directement après minuit, directement avant minuit et même directement à minuit. Cela signifie que pour chaque test, il y aura quatre temps de base par rapport auxquels il est testé.
Ensuite, vous ajouteriez systématiquement des cas marginaux à toutes les autres classes. Vous avez le test pour aujourd'hui. Donc, ajoutez un moment juste avant et après le comportement devrait changer. La même chose pour hier. La même chose il y a une semaine, etc.
Il y a de fortes chances qu'en énumérant tous les cas critiques de manière systématique et en écrivant des cas tests pour eux, vous découvrez que votre spécification manque de détails et vous l'ajoutez. Notez que le traitement des dates est souvent un problème, car les gens oublient souvent d’écrire leurs tests pour pouvoir les exécuter à des moments différents.
Notez, cependant, que la plupart de ce que j'ai écrit a peu à voir avec le TDD. Il s'agit d'écrire des classes d'équivalence et de s'assurer que vos propres spécifications sont suffisamment détaillées à leur sujet. C'est le processus avec lequel vous minimisez les erreurs logiques. TDD s'assure simplement que votre code est conforme à votre modèle mental.
Venir avec des cas de test est difficile . Les tests fondés sur la classe d'équivalence ne sont pas la fin, et dans certains cas, ils peuvent augmenter considérablement le nombre de tests. Dans le monde réel, l'ajout de tous ces tests n'est souvent pas économiquement viable (même si en théorie, cela devrait être fait).
la source
Pourquoi pas? Cela semble être une très bonne idée!
L'ajout de contrats (assertions) au code est un moyen assez solide d'améliorer son exactitude. Généralement, nous les ajoutons en tant que conditions préalables à la saisie d'une fonction et postconditions au retour d'une fonction. Par exemple, nous pourrions ajouter une postcondition que toutes les valeurs retournées sont soit de forme « A [unité] Il y a » ou « [numéro] [unité] il y a s ». Lorsque cela est fait de manière disciplinée, cela conduit à la conception par contrat et constitue l'un des moyens les plus courants d'écrire du code hautement sécurisé.
De manière critique, les contrats ne sont pas destinés à être testés; ce sont autant de spécifications de votre code que vos tests. Cependant, vous pouvez tester via les contrats: appelez le code de votre test et, si aucun contrat ne génère d'erreur, le test réussit. Faire une boucle à chaque seconde des dix dernières années, c'est un peu long. Mais nous pouvons utiliser un autre style de test appelé test basé sur les propriétés .
Dans PBT au lieu de tester des sorties spécifiques du code, vous testez que la sortie obéit à une propriété. Par exemple, une propriété d'une
reverse()
fonction est que pour une listel
,reverse(reverse(l)) = l
. L'avantage d'écrire de tels tests est que le moteur PBT peut générer quelques centaines de listes arbitraires (et quelques pathologiques) et vérifier qu'elles possèdent toutes cette propriété. Si tel n'est pas le cas , le moteur "réduit" le cas d'échec pour trouver une liste minimale qui casse votre code. On dirait que vous écrivez Python, qui a Hypothesis comme principal framework PBT.Par conséquent, si vous souhaitez trouver un moyen efficace de trouver des solutions plus complexes que celles auxquelles vous n’allez pas penser, l’utilisation combinée de contrats et de tests basés sur les propriétés aidera beaucoup. Bien entendu, cela ne remplace pas les tests unitaires d’écriture, mais l’augmente, ce qui est vraiment ce que nous pouvons faire de mieux en tant qu’ingénieurs.
la source
/(today)|(yesterday)|([2-6] days ago)|...
) et vous pouvez ensuite exécuter le processus avec des entrées sélectionnées de manière aléatoire jusqu'à ce que vous en trouviez une qui ne figure pas dans l'ensemble des sorties attendues. Cette approche aurait attrapé ce bogue et n'aurait pas nécessité de se rendre compte que le bogue existait peut-être auparavant.C'est un exemple où il aurait été utile d'ajouter un peu de modularité. Si un segment de code source d'erreurs est utilisé plusieurs fois, il est recommandé de l'envelopper dans une fonction si possible.
la source
TDD fonctionne mieux comme technique si la personne qui écrit les tests est contradictoire. Ceci est difficile si vous ne programmez pas en binôme. Une autre façon de penser à cela est:
C'est un art différent, qui s'applique à l'écriture de code correct avec ou sans TDD, et peut-être aussi complexe (sinon plus) que l'écriture de code. C'est quelque chose que vous devez pratiquer, et c'est quelque chose qu'il n'y a pas de réponse simple, facile et simple à la question.
La technique de base pour écrire un logiciel robuste est également la technique de base pour comprendre comment écrire des tests efficaces:
Comprendre les conditions préalables à une fonction - les états valides (c.-à-d. Quelles hypothèses faites-vous sur l'état de la classe dont la fonction est une méthode) et les plages de paramètres d'entrée valides - chaque type de données a une plage de valeurs possibles - un sous-ensemble sera traité par votre fonction.
Si vous ne faites que tester explicitement ces hypothèses lors de la saisie d'une fonction et vous assurer qu'une violation est consignée ou générée et / ou que des erreurs de fonction se produisent sans autre traitement, vous pouvez rapidement savoir si votre logiciel échoue en production. et tolérant aux erreurs, et développez vos compétences en rédaction de tests contradictoires.
NB Il existe toute une littérature sur les conditions préalables et postérieures, les invariants, etc., ainsi que sur les bibliothèques qui peuvent les appliquer à l'aide d'attributs. Personnellement, je ne suis pas fan de la formalité, mais ça vaut la peine de regarder.
la source
C’est l’un des faits les plus importants en matière de développement logiciel: Il est absolument impossible d’écrire du code exempt de bogues.
TDD ne vous évitera pas d'introduire des bogues correspondant à des cas de test auxquels vous n'aviez pas pensé. Cela vous évitera également d'écrire un test incorrect sans vous en rendre compte, puis d'écrire un code incorrect qui réussira le test du buggy. Et chaque technique de développement logiciel unique jamais créée possède des trous similaires. En tant que développeurs, nous sommes des êtres humains imparfaits. À la fin de la journée, il n’existe aucun moyen d’écrire du code 100% sans bug. Cela ne s'est jamais produit et ne se produira jamais.
Cela ne veut pas dire que vous devriez perdre espoir. Bien qu’il soit impossible d’écrire du code parfaitement parfait, il est tout à fait possible d’écrire du code qui contient si peu de bogues qui apparaissent dans des cas aussi rares que le logiciel est extrêmement pratique à utiliser. Un logiciel qui ne présente pas de comportement buggy en pratique est très possible d’écrire.
Mais pour l'écrire, nous devons accepter le fait que nous allons produire un logiciel buggy. Presque toutes les pratiques modernes de développement de logiciels sont à un certain niveau construites autour de la prévention des bogues ou de la protection contre les conséquences des bogues que nous produisons inévitablement:
La solution ultime au problème que vous avez identifié n’est pas de lutter contre le fait que vous ne pouvez pas garantir que vous écrivez un code exempt de bogues, mais plutôt de l’accepter. Adoptez les meilleures pratiques de l’industrie dans tous les domaines de votre processus de développement et vous livrerez systématiquement à vos utilisateurs du code qui, sans être tout à fait parfait, est suffisamment robuste pour le poste.
la source
Vous n'aviez simplement pas pensé à ce cas auparavant et vous n'aviez donc pas de cas test.
Cela arrive tout le temps et est juste normal. Il faut toujours faire un compromis sur les efforts que vous déployez pour créer tous les scénarios de test possibles. Vous pouvez passer un temps infini à examiner tous les cas de test.
Pour un pilote automatique d'aéronef, vous passeriez beaucoup plus de temps que pour un simple outil.
Il est souvent utile de réfléchir aux plages valides de vos variables d’entrée et de tester ces limites.
De plus, si le testeur est une personne différente de celle du développeur, des cas plus importants sont souvent détectés.
la source
C'est une autre erreur logique dans votre code pour laquelle vous n'avez pas encore de test unitaire :) - votre méthode renverra des résultats incorrects pour les utilisateurs dont les fuseaux horaires ne correspondent pas à l'heure UTC. Vous devez convertir "maintenant" et la date de l'événement en fuseau horaire local de l'utilisateur avant de procéder au calcul.
Exemple: en Australie, un événement a lieu à 9 heures, heure locale. À 11h, il sera affiché comme "hier" car la date UTC a changé.
la source
Laissez quelqu'un d'autre écrire les tests. De cette façon, une personne peu familiarisée avec votre implémentation peut vérifier des situations rares auxquelles vous n'avez pas pensé.
Si possible, injectez des cas de test en tant que collections. Cela rend l'ajout d'un autre test aussi simple que l'ajout d'une autre ligne, comme
yield return new TestCase(...)
. Cela peut aller dans le sens des tests exploratoires , en automatisant la création de scénarios de test: "Voyons ce que le code retourne pour toutes les secondes d'il y a une semaine".la source
Vous semblez avoir l’idée fausse que si tous vos tests réussissent, vous n’aurez aucun bogue. En réalité, si tous vos tests réussissent, tout le comportement connu est correct. Vous ne savez toujours pas si le comportement inconnu est correct ou non.
J'espère que vous utilisez la couverture de code avec votre TDD. Ajoutez un nouveau test pour le comportement inattendu. Ensuite, vous pouvez exécuter le test du comportement inattendu pour voir quel chemin il emprunte réellement dans le code. Une fois que vous connaissez le comportement actuel, vous pouvez apporter une modification pour le corriger et, lorsque tous les tests seront à nouveau réussis, vous saurez que vous l'avez fait correctement.
Cela ne signifie toujours pas que votre code est exempt de bogues, mais qu'il est meilleur qu'avant et, encore une fois, tout le comportement connu est correct!
Utiliser correctement TDD ne signifie pas que vous écrirez du code sans bogue, cela signifie que vous écrirez moins de bogues. Vous dites:
Cela signifie-t-il que le comportement plus d'un jour, mais pas hier, a été spécifié dans les exigences? Si vous avez manqué une exigence écrite, c'est de votre faute. Si vous réalisiez que les exigences étaient incomplètes car vous les codiez, tant mieux! Si tout le monde qui a travaillé sur les exigences a manqué ce cas, vous n'êtes pas pire que les autres. Tout le monde fait des erreurs, et plus elles sont subtiles, plus elles sont faciles à manquer. La grande emporter ici est que TDD n'empêche toutes les erreurs!
la source
Oui. Le développement piloté par les tests ne change pas cela. Vous pouvez toujours créer des bogues dans le code actuel, ainsi que dans le code de test.
Oh, mais ça l'a fait! Tout d'abord, lorsque vous avez remarqué le bogue, vous disposiez déjà du framework de test complet et vous deviez simplement le corriger dans le test (et le code lui-même). Deuxièmement, vous ne savez pas combien de bugs supplémentaires vous auriez eu si vous n'aviez pas fait TDD au début.
Tu ne peux pas. Même la NASA n'a pas trouvé le moyen d'éviter les bugs; nous autres êtres humains, certainement pas.
C'est une erreur. L'un des principaux avantages de TDD est que vous pouvez coder avec moins de réflexion, car tous ces tests permettent au moins de bien capturer les régressions. De plus, même ou particulièrement avec TDD, il n’est pas prévu que le code soit exempt de bogues (sinon votre vitesse de développement sera tout simplement arrêtée).
Cela serait clairement en contradiction avec le principe de coder uniquement ce dont vous avez réellement besoin pour le moment. Vous pensiez avoir besoin de ces cas, et c'était donc le cas. C'était un morceau de code non critique; comme vous l'avez dit, il n'y a pas eu de dégâts sauf que vous vous en êtes demandé pendant 30 minutes.
Pour le code essentiel à la mission, vous pouvez réellement faire ce que vous avez dit, mais pas pour votre code standard quotidien.
Vous pas. Vous faites confiance à vos tests pour trouver la plupart des régressions; vous vous tenez au cycle rouge-vert-refactor, en écrivant des tests avant / pendant le codage réel, et (important!) vous implémentez la quantité minimale nécessaire pour faire le commutateur rouge-vert (ni plus, ni moins). Cela aboutira à une excellente couverture de test, au moins positive.
Lorsque vous trouvez un bogue, et non si vous le trouvez, vous écrivez un test pour reproduire ce bogue et corrigez le bogue avec le moins de travail possible pour que ledit test passe du rouge au vert.
la source
Vous venez de découvrir que, quels que soient vos efforts, vous ne pourrez jamais attraper tous les bogues possibles dans votre code.
Cela signifie donc que même tenter d'attraper tous les bogues est un exercice futile et que vous ne devriez utiliser que des techniques telles que TDD comme moyen d'écrire un code plus performant, un code comportant moins de bogues et non pas 0.
Cela signifie que vous devriez passer moins de temps à utiliser ces techniques et gagner du temps à travailler sur des méthodes alternatives pour trouver les bogues qui se glissent dans le réseau de développement.
des alternatives telles que les tests d'intégration, ou une équipe de test, les tests de système, ainsi que la journalisation et l'analyse de ces journaux.
Si vous ne pouvez pas attraper tous les insectes, vous devez avoir une stratégie en place pour atténuer les effets des insectes qui vous échappent. Si vous devez le faire de toute façon, il est plus logique de déployer davantage d'efforts dans ce sens que d'essayer (en vain) de les arrêter.
Après tout, il est inutile de dépenser une fortune en temps pour la rédaction de tests et le premier jour où vous donnez votre produit à un client, il tombe, en particulier si vous ne savez pas comment trouver et résoudre ce problème. La résolution des bogues post-mortem et post-livraison est si importante et nécessite plus d’attention que la plupart des gens ne consacrent à la rédaction de tests unitaires. Enregistrez les tests unitaires pour les éléments compliqués et n'essayez pas de perfection à la perfection.
la source
That in turn means you should spend less time using these techniques
- Mais vous venez de dire que ça va aider avec moins de bugs?!