Comment éviter les erreurs logiques dans le code, quand TDD n'a pas aidé?

67

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 deltaest 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?

Arseni Mourzenko
la source
38
En ayant un cas de test pour cela? Cela ressemble à la façon dont vous l'avez découvert par la suite, et en prise avec TDD.
ousurous
63
Vous venez de comprendre pourquoi je ne suis pas un partisan du développement piloté par les tests: selon mon expérience, la plupart des bugs détectés en production sont des scénarios auxquels personne n'a pensé. Le développement piloté par les tests et les tests unitaires ne font rien pour cela. (Cependant, les tests unitaires ont un intérêt dans la détection de bugs introduits lors de futures modifications.)
Loren Pechtel
102
Répétez après moi: "Il n’ya pas de balles en argent, y compris le TDD." Il n’existe aucun processus, aucun ensemble de règles, aucun algorithme à suivre de manière robotique pour produire un code parfait. Si c'était le cas, nous pourrions automatiser l'ensemble du processus et en terminer avec ce processus.
jpmc26
43
Félicitations, vous avez retrouvé la sagesse selon laquelle aucun test ne peut prouver l’absence de bugs. Mais si vous recherchez des techniques pour créer une meilleure couverture du domaine d'entrée possible, vous devez analyser en profondeur le domaine, les cas extrêmes et les classes d'équivalence de ce domaine. Toutes les techniques anciennes connues depuis longtemps avant l’invention du terme TDD.
Doc Brown
80
Je n'essaie pas d'être sournois, mais votre question pourrait apparemment être reformulée comme suit: "comment puis-je penser à des choses auxquelles je n'ai pas pensé?". Je ne sais pas ce que cela a à voir avec le TDD.
Jared Smith

Réponses:

57

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é):

def pluralize(num, unit):
    if num == 1:
        return unit
    else:
        return unit + "s"

def convert_to_unit(delta, unit):
    factor = 1
    if unit == "week":
        factor = 7 
    elif unit == "month":
        factor = 30
    elif unit == "year":
        factor = 365
    return delta // factor

def best_unit(delta):
    if delta < 7:
        return "day"
    elif delta < 30:
        return "week"
    elif delta < 365:
        return "month"
    else:
        return "year"

def human_friendly(event_date):
    date = event_date.date()
    today = now.date()
    yesterday = today - datetime.timedelta(1)
    if date == today:
        return "Today"
    elif date == yesterday:
        return "Yesterday"
    else:
        delta = (now - event_date).days
        unit = best_unit(delta)
        converted = convert_to_unit(delta, unit)
        pluralized = pluralize(converted, unit)
        return "{} {} ago".format(converted, pluralized)

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_unitfaire si deltaest 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.

Karl Bielefeldt
la source
12
La prochaine étape consiste à internationaliser, et pluralizetravailler uniquement pour un sous-ensemble de mots anglais sera un handicap.
Déduplicateur
@Deduplicator sûr, mais selon quelles langues / cultures que vous visez, vous pourriez sortir avec seulement modifier l' pluralizeutilisation numet unitde 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 ;-)
Hulk
4
Un problème subsiste même avec cette refactorisation, qui est que "hier" n'a pas beaucoup de sens aux petites heures du matin (peu après 12h01). En termes d’amitié humaine, quelque chose qui s’est passé à 23h59 ne change pas soudainement d’aujourd’hui à hier, alors que l’horloge passe à minuit. Au lieu de cela, il passe de «il y a 1 minute» à «il y a 2 minutes». "Aujourd'hui" est trop grossier en ce qui concerne ce qui s'est passé il y a quelques minutes à peine et "hier" regorge de problèmes pour les noctambules.
David Hammen
@ DavidHammen Ceci est un problème de facilité d'utilisation et cela dépend de la précision dont vous avez besoin. Quand vous voulez savoir au moins jusqu'à l'heure, je ne dirais pas que "hier" est bien. "Il y a 24 heures" est beaucoup plus clair et est une expression humaine couramment utilisée pour souligner le nombre d'heures. Les ordinateurs qui essaient d'être "humains" se trompent presque toujours et le généralisent à "hier", ce qui est trop vague. Mais pour le savoir, vous devrez interroger les utilisateurs pour savoir ce qu’ils en pensent. Pour certaines choses, vous voulez vraiment la date et l'heure exactes, donc "hier" est toujours faux.
Brandin
149

Le développement piloté par les tests n'a pas aidé.

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.

esoterik
la source
2
On pourrait faire valoir que si le développeur n'avait pas utilisé tdd, il aurait beaucoup plus de chances de rater d'autres cas également.
Caleb
75
Et en plus de cela, pensez à combien de temps a été gagné quand ils corrigent le bogue? En mettant en place les tests existants, ils ont immédiatement compris que leur changement n'avait pas brisé le comportement existant. Et ils étaient libres d'ajouter les nouveaux cas de test et le refactor sans avoir à exécuter de nombreux tests manuels par la suite.
Caleb
15
TDD est seulement aussi bon que les tests écrits.
Mindwin
Autre observation: l'ajout du test pour ce cas améliorera la conception, en nous obligeant à le datetime.utcnow()retirer de la fonction et à le remplacer nowpar un argument (reproductible).
Toby Speight
114

un événement qui se passe il y a vingt six heures serait il y a un jour

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.

Kevin Krumwiede
la source
45
C'est un excellent point à faire valoir. Manquer une exigence ne signifie pas nécessairement que votre processus de mise en œuvre a échoué. Cela signifie simplement que l'exigence n'était pas bien définie. (Ou vous avez simplement commis une erreur humaine, ce qui arrivera de temps en temps)
Caleb
C'est la réponse que je voulais faire. Je définirais la spécification comme suit: "si l'événement correspond à ce jour du calendrier, delta actuel en heures. Sinon, utilisez les dates uniquement pour déterminer le delta" Les heures de test ne sont utiles que dans une journée, si au-delà, votre résolution est censée être des jours.
Baldrickk
1
J'aime cette réponse car elle met en évidence le vrai problème: les dates et les heures sont deux quantités différentes. Ils sont liés, mais lorsque vous commencez à les comparer, les choses vont vite au sud. En programmation, la logique de la date et de l’heure est l’une des choses les plus difficiles à maîtriser. Je n'aime vraiment pas que beaucoup d'implémentations de date stockent la date à 0:00 heure. Cela crée beaucoup de confusion.
Pieter B
38

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?

Ian Jacobs
la source
2
Avoir des tests écrits par quelqu'un d'autre que le développeur est toujours une bonne idée, cela signifie que les deux parties doivent ignorer la même condition d'entrée pour que le bogue puisse être transformé en production.
Michael Kay
35

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.

cbojar
la source
9
C’est une partie très importante du TDD qui est souvent négligée et que j’ai rarement vu parler dans les articles et les guides - c’est vraiment important de tester les cas extrêmes et les conditions aux limites car je trouve que c’est la source de 90% des bogues - d’ici. -une erreurs, débordements et débordements, dernier jour du mois, dernier mois de l'année, années bissextiles, etc., etc.
GoatInTheMachine
2
@GoatInTheMachine - et 90% de ces 90% des bogues sont sur le point de faire la transition vers l'heure avancée ..... Hahaha
Caleb
1
Vous pouvez d'abord diviser les entrées possibles dans les classes d'équivalence , puis déterminer les cas de bord aux limites des classes. De notre part, cet effort peut être plus important que l’effort de développement; Que cela en vaille la peine dépend de l’importance de fournir des logiciels aussi exempts d’erreurs que possible, de l’échéance, des coûts et de la patience dont vous disposez.
Peter - Réintégrer Monica le
2
C'est la bonne réponse. Un grand nombre de règles commerciales exigent que vous divisiez une plage de valeurs en intervalles où elles doivent être traitées de différentes manières.
Abuzittin gillifirca
14

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

Polygone
la source
12

La seule façon dont je puisse penser est d’ajouter beaucoup d’affirmations pour les cas qui, à mon avis, ne se produiraient jamais (comme j’ai pensé qu’il ya un jour, c’est nécessairement hier), puis de passer en revue chaque seconde ces dix dernières années, en vérifiant toute violation d'assertion, ce qui semble trop complexe.

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 liste l, 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.

Hovercouch
la source
2
C'est exactement la bonne solution à ce genre de problème. L'ensemble des sorties valides est facile à définir (vous pouvez donner une expression régulière très simplement, par exemple /(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.
Jules
@Jules Voir aussi vérification / test de propriété . J'écris habituellement des tests de propriétés au cours du développement, afin de couvrir autant de cas imprévus que possible et de m'obliger à penser à des propriétés générales / invariants. Je garde des tests
uniques
1
Si vous faites beaucoup de boucle dans les tests, cela prendra très longtemps, ce qui va à l’encontre de l’un des objectifs principaux des tests unitaires: exécuter les tests rapidement !
CJ Dennis
5

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.

def time_ago(delta, unit):
    delta_str = _number_to_text(delta) + " " + unit;
    if delta == 1:
        return delta_str + " ago"
    else:
        return delta_str = "s ago"

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 time_ago(delta, "day")

if delta < 30:
    weeks = math.floor(delta / 7)
    return time_ago(weeks, "week")

if delta < 365:
    months = math.floor(delta / 31)
    return time_ago(months, "month")
Antonio Perez
la source
5

Le développement piloté par les tests n'a pas aidé.

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:

  • N'écrivez pas de tests pour confirmer que la fonction testée fonctionne comme vous l'avez faite. Ecrire des tests qui délibérément le casser.

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.

Chris Becke
la source
1

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:

  • Rassembler les exigences nous permet de savoir à quoi ressemble un comportement incorrect dans notre code.
  • L'écriture de code propre et soigneusement architecturé permet d'éviter plus facilement d'introduire des bogues et de les résoudre plus facilement lorsque nous les identifions.
  • L’écriture de tests nous permet de produire un enregistrement de ce que nous pensons être l’un des pires bugs possibles de notre logiciel et de prouver que nous évitons au moins ces bugs. TDD produit ces tests avant le code, BDD les déduit de la configuration requise et les tests unitaires obsolètes génèrent des tests après l'écriture du code, mais ils empêchent tous les régressions les pires à l'avenir.
  • Les examens par les pairs signifient que chaque fois que le code est modifié, au moins deux paires d'yeux ont vu le code, ce qui réduit la fréquence à laquelle les bogues glissent dans le maître.
  • Utiliser un traqueur de bogues ou un traqueur de récits d'utilisateurs qui traite les bogues comme des récits d'utilisateurs signifie que, lorsque des bogues apparaissent, ils sont suivis et finalement traités, pas oubliés et laissés de manière cohérente pour les utilisateurs.
  • L'utilisation d'un serveur de transfert signifie qu'avant une version majeure, tous les bogues bloquants ont une chance d'apparaître et d'être traités.
  • L'utilisation du contrôle de version signifie que, dans le pire des cas, lorsqu'un code contenant des bogues majeurs est envoyé aux clients, vous pouvez effectuer une restauration d'urgence et remettre un produit fiable entre les mains de vos clients pendant que vous triez les problèmes.

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.

Kevin - Rétablir Monica
la source
1

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.

Simon
la source
1

(et en pensant qu'il s'agit de fuseaux horaires, malgré l'utilisation uniforme du code UTC dans le code)

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

Sergey
la source
0
  • 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".

nul
la source
0

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:

Les exigences étaient relativement claires

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!

CJ Dennis
la source
0

Il est très facile de commettre une erreur logique même dans un code source aussi simple.

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.

Le développement piloté par les tests n'a pas aidé.

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.

Ce qui est également inquiétant, c'est que je ne vois pas comment on pourrait éviter de tels insectes.

Tu ne peux pas. Même la NASA n'a pas trouvé le moyen d'éviter les bugs; nous autres êtres humains, certainement pas.

Outre réfléchir avant d'écrire du code,

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

la seule façon de penser est d’ajouter beaucoup d’affirmations pour les cas qui, à mon avis, ne se produiraient jamais (comme j’y croyais il ya un jour, c'est forcément hier), puis de passer en revue toutes les secondes ces dix dernières années, en vérifiant toute violation d'assertion, ce qui semble trop complexe.

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.

Comment pourrais-je éviter de créer ce bogue en premier lieu?

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.

AnoE
la source
-2

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.

gbjbaanb
la source
C'est extrêmement défaitest. 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?!
JᴀʏMᴇᴇ
@ JᴀʏMᴇᴇ plus une attitude pragmatique de la technique qui vous vaudra le plus pour votre argent.Je connais des gens fiers de passer 10 fois à écrire des tests qu'ils ne le faisaient sur leur code, et qu'ils ont toujours des bugs . sur les techniques de test est essentielle. Et les tests d'intégration doivent être utilisés de toute façon, alors mettez plus d'effort dessus que dans les tests unitaires.
gbjbaanb