Comment corriger une erreur dans le test, après avoir écrit l'implémentation

21

Quelle est la meilleure ligne de conduite dans TDD si, après avoir correctement implémenté la logique, le test échoue toujours (car il y a une erreur dans le test)?

Par exemple, supposons que vous souhaitiez développer la fonction suivante:

int add(int a, int b) {
    return a + b;
}

Supposons que nous le développions dans les étapes suivantes:

  1. Test d'écriture (pas encore de fonction):

    // test1
    Assert.assertEquals(5, add(2, 3));
    

    Résultats en erreur de compilation.

  2. Écrivez une implémentation de fonction factice:

    int add(int a, int b) {
        return 5;
    }
    

    Résultat: test1passe.

  3. Ajoutez un autre scénario de test:

    // test2 -- notice the wrong expected value (should be 11)!
    Assert.assertEquals(12, add(5, 6));
    

    Résultat: test2échoue, test1passe toujours.

  4. Écrivez une implémentation réelle:

    int add(int a, int b) {
        return a + b;
    }
    

    Résultat: test1passe test2toujours, échoue toujours (depuis 11 != 12).

Dans ce cas particulier: serait-il préférable de:

  1. correct test2, et voyez qu'il passe maintenant, ou
  2. supprimez la nouvelle partie de l'implémentation (c.-à-d. revenez à l'étape 2 ci-dessus), corrigez test2et laissez-la échouer, puis réintroduisez l'implémentation correcte (étape 4 ci-dessus).

Ou existe-t-il un autre moyen plus intelligent?

Bien que je comprenne que l'exemple du problème est plutôt trivial, je suis intéressé par ce qu'il faut faire dans le cas générique, qui pourrait être plus complexe que l'ajout de deux nombres.

EDIT (En réponse à la réponse de @Thomas Junk):

Le point central de cette question est ce que TDD suggère dans un tel cas, et non ce qui est "la meilleure pratique universelle" pour obtenir un bon code ou des tests (qui pourraient être différents de la méthode TDD).

Attilio
la source
3
Le refactoring contre la barre rouge est un concept pertinent.
RubberDuck
5
De toute évidence, vous devez faire TDD sur votre TDD.
Blrfl
17
Si quelqu'un me demande pourquoi je suis sceptique vis-à-vis du TDD, je lui indiquerai cette question. C'est kafkaïen.
Traubenfuchs
@Blrfl c'est ce que Xibit nous dit »J'ai mis le TDD dans TDD pour que vous puissiez TDD pendant TDDing«: D
Thomas Junk
3
@Traubenfuchs J'admets que la question semble idiote à première vue et je ne suis pas partisan de "faire TDD tout le temps", mais je crois qu'il y a un grand avantage à voir un test échouer, puis écrire du code qui fait passer le test (qui est en fait le sujet de cette question, après tout).
Vincent Savard

Réponses:

31

La chose absolument critique est que vous voyez le test réussir et échouer.

Que vous supprimiez le code pour faire échouer le test, puis le réécriviez ou le glissiez dans le presse-papiers pour le coller plus tard n'a pas d'importance. TDD n'a jamais dit que vous deviez retaper quoi que ce soit. Il veut savoir que le test réussit uniquement lorsqu'il doit réussir et échoue uniquement lorsqu'il doit échouer.

Voir le test réussir et échouer est la façon dont vous testez le test. Ne faites jamais confiance à un test que vous n'avez jamais vu faire les deux.


Refactoring Against The Red Bar nous donne des étapes formelles pour refactoring d'un test de travail:

  • Exécutez le test
    • Notez la barre verte
    • Brisez le code testé
  • Exécutez le test
    • Notez la barre rouge
    • Refactoriser le test
  • Exécutez le test
    • Notez la barre rouge
    • Décompressez le code testé
  • Exécutez le test
    • Notez la barre verte

Cependant, nous ne refactorisons pas un test de travail. Nous devons transformer un test de buggy. Une préoccupation est le code qui a été introduit alors que seul ce test le couvrait. Ce code devrait être annulé et réintroduit une fois le test corrigé.

Si ce n'est pas le cas et que la couverture du code n'est pas un problème en raison d'autres tests couvrant le code, vous pouvez transformer le test et l'introduire en tant que test vert.

Ici, le code est également annulé, mais juste assez pour entraîner l'échec du test. Si cela ne suffit pas pour couvrir tout le code introduit alors qu'il n'est couvert que par le test de buggy, nous avons besoin d'un plus grand retour en arrière du code et de plus de tests.

Introduire un test vert

  • Exécutez le test
    • Notez la barre verte
    • Brisez le code testé
  • Exécutez le test
    • Notez la barre rouge
    • Décompressez le code testé
  • Exécutez le test
    • Notez la barre verte

Briser le code peut être commenter le code ou le déplacer ailleurs pour le coller plus tard. Cela nous montre l'étendue du code couvert par le test.

Pour ces deux dernières courses, vous êtes de retour dans le cycle vert-rouge normal. Vous collez simplement au lieu de taper pour annuler le code et faire passer le test. Assurez-vous donc de ne coller que la quantité suffisante pour réussir le test.

Le motif global ici est de voir la couleur du test changer comme nous l'attendons. Notez que cela crée une situation où vous avez brièvement un test vert non fiable. Faites attention à ne pas être interrompu et à oublier où vous en êtes dans ces étapes.

Mes remerciements à RubberDuck pour le lien Embracing the Red Bar .

candied_orange
la source
2
Je préfère cette réponse: il est important de voir le test échouer avec un code incorrect, donc je supprimerais / commenterais le code, corrigerais les tests et les verrais échouer, remettre le code (peut-être introduire une erreur délibérée pour mettre les tests à le test) et corrigez le code pour le faire fonctionner. C'est très XP de le supprimer et de le réécrire complètement, mais parfois il suffit d'être pragmatique. ;)
GolezTrol
@GolezTrol Je pense que ma réponse dit la même chose, donc j'apprécierais tout commentaire que vous auriez sur si cela n'était pas clair.
jonrsharpe
@jonrsharpe Votre réponse est bonne aussi, et je l'ai votée avant même de lire celle-ci. Mais lorsque vous êtes très strict dans le retour du code, CandiedOrange suggère une approche plus pragmatique qui me plaît davantage.
GolezTrol
@GolezTrol Je n'ai pas dit comment rétablir le code; commentez-le, coupez-le, collez-le, utilisez l'historique de votre IDE; cela n'a pas vraiment d'importance. La chose cruciale est pourquoi vous le faites: vous pouvez donc vérifier que vous obtenez le bon échec. J'ai édité, je l'espère, pour clarifier.
jonrsharpe
10

Quel est l' objectif global que vous souhaitez atteindre?

  • Faire de bons tests?

  • Faire la bonne mise en œuvre?

  • Faire le TTD religieusement bien ?

  • Aucune de ces réponses?

Vous pensez peut-être trop votre relation avec les tests et les tests.

Les tests ne garantissent pas l' exactitude d'une implémentation. La réussite de tous les tests ne dit rien sur la question de savoir si votre logiciel fait ce qu'il doit; il ne fait aucune déclaration essentialiste sur votre logiciel.

Prenons votre exemple:

L'implémentation "correcte" de l'addition serait le code équivalent à a+b. Et tant que votre code fait cela, vous diriez que l'algorithme est correct dans ce qu'il fait et qu'il est correctement implémenté.

int add(int a, int b) {
    return a + b;
}

À première vue , nous serions tous deux d'accord pour dire qu'il s'agit de la mise en œuvre d'un ajout.

Mais ce que nous faisons n'est pas vraiment de dire que ce code est l'implémentation de additioncelui - ci ne se comporte qu'à un certain degré comme celui-ci: pensez au débordement d'entier .

Le débordement d'entier se produit dans le code, mais pas dans le concept de addition. Donc: votre code se comporte dans une certaine mesure comme le concept de addition, mais ne l'est pas addition.

Ce point de vue plutôt philosophique a plusieurs conséquences.

Et on peut dire que les tests ne sont rien de plus que des hypothèses de comportement attendu de votre code. En testant votre code, vous pourriez (peut-être) ne jamais vous assurer que votre implémentation est correcte , le mieux que vous puissiez dire est que vos attentes sur les résultats que votre code fournit ont été ou n'ont pas été satisfaites; que ce soit votre code qui soit faux, que ce soit votre test qui soit faux ou que ce soit les deux tous les deux.

Des tests utiles vous aident à fixer vos attentes sur ce que le code doit faire: tant que je ne change pas mes attentes et tant que le code modifié me donne le résultat que j'attends, je peux être sûr, que les hypothèses que j'ai faites à propos de les résultats semblent fonctionner.

Cela n'aide pas lorsque vous faites de fausses hypothèses; Mais salut! au moins elle prévient la schizophrénie: s'attendre à des résultats différents alors qu'il ne devrait pas y en avoir.


tl; dr

Quelle est la meilleure ligne de conduite dans TDD si, après avoir correctement implémenté la logique, le test échoue toujours (car il y a une erreur dans le test)?

Vos tests sont des hypothèses sur le comportement du code. Si vous avez de bonnes raisons de penser que votre implémentation est correcte, corrigez le test et voyez si cette hypothèse se vérifie.

Thomas Junk
la source
1
Je pense que la question sur les objectifs généraux est assez importante, merci de l'avoir soulevée. Pour moi, le prio le plus élevé est le suivant: 1. mise en œuvre correcte 2. tests "sympas" (ou, je dirais plutôt "tests" utiles "/" bien conçus "). Je considère le TDD comme un outil possible pour atteindre ces deux objectifs. Donc, même si je ne veux pas nécessairement suivre religieusement TDD, dans le contexte de cette question, je suis surtout intéressé par la perspective TDD. Je vais modifier la question pour clarifier cela.
Attilio
Alors, écririez-vous un test qui teste le débordement et passe quand il se produit ou le feriez-vous échouer quand il se produit parce que l'algorithme est l'addition et le débordement produit la mauvaise réponse?
Jerry Jeremiah
1
@JerryJeremiah Mon point est le suivant: ce que vos tests doivent couvrir dépend de votre cas d'utilisation. Pour un cas d'utilisation où vous additionnez un tas de chiffres simples, l'algorithme est assez bon . Si vous savez qu'il est très probable que vous additionniez des «grands nombres», datatypec'est clairement le mauvais choix. Un test révélerait que: votre attente serait «fonctionne pour les grands nombres» et n'est dans plusieurs cas pas satisfaite. La question serait alors de savoir comment traiter ces cas. S'agit-il de cas d'angle? Si oui, comment y faire face? Peut-être que certaines clauses quard aident à éviter un plus grand désordre. La réponse est liée au contexte.
Thomas Junk
7

Vous devez savoir que le test échouera si la mise en œuvre est incorrecte, ce qui n'est pas la même chose que si la mise en œuvre est correcte. Par conséquent, vous devez remettre le code dans un état où vous vous attendez à ce qu'il échoue avant de corriger le test, et vous assurer qu'il échoue pour la raison que vous attendiez (c.-à-d. 5 != 12), Plutôt que quelque chose d'autre que vous n'aviez pas prévu .

jonrsharpe
la source
Comment pouvons-nous vérifier que le test échoue pour la raison à laquelle nous nous attendons?
Basilevs
2
@Basilevs vous: 1. faites une hypothèse quant à la raison de l'échec; 2. lancez le test; et 3. lire le message d'échec résultant et comparer. Parfois, cela suggère également des façons de réécrire le test pour vous donner une erreur plus significative (par exemple, assertTrue(5 == add(2, 3))donne une sortie moins utile que assertEqual(5, add(2, 3))même s'ils testent tous les deux la même chose).
jonrsharpe
On ne sait toujours pas comment appliquer ce principe ici. J'ai une hypothèse - le test renvoie une valeur constante, comment une nouvelle exécution du même test garantirait-elle que j'ai raison? Évidemment, pour tester cela, j'ai besoin d'un autre test. Je suggère d'ajouter un exemple explicite pour répondre.
Basilevs
1
@Basilevs quoi? Votre hypothèse ici à l'étape 3 serait "le test échoue car 5 n'est pas égal à 12" . L'exécution du test vous montrera si le test échoue pour cette raison, dans quel cas vous continuez, ou pour une autre raison, dans ce cas, vous comprendrez pourquoi. C'est peut-être un problème de langue, mais je ne comprends pas très bien ce que vous proposez.
jonrsharpe
5

Dans ce cas particulier, si vous changez le 12 en 11, et que le test passe maintenant, je pense que vous avez fait un bon travail pour tester le test ainsi que la mise en œuvre, donc il n'y a pas grand-chose à passer par des cercles supplémentaires.

Cependant, le même problème peut survenir dans des situations plus complexes, comme lorsque vous avez une erreur dans votre code d'installation. Dans ce cas, après avoir corrigé votre test, vous devriez probablement essayer de muter votre implémentation de manière à faire échouer ce test particulier, puis annuler la mutation. Si le retour de l'implémentation est la manière la plus simple de le faire, alors c'est très bien. Dans votre exemple, vous pouvez muter a + ben a + aou a * b.

Alternativement, si vous pouvez muter légèrement l'assertion et voir l'échec du test, cela peut être assez efficace pour tester le test.

Vaughn Cato
la source
0

Je dirais que c'est un cas pour votre système de contrôle de version préféré:

  1. Organisez la correction du test en conservant vos modifications de code dans votre répertoire de travail.
    Validez avec un message correspondant Fixed test ... to expect correct output.

    Avec git, cela peut nécessiter l'utilisation de git add -psi test et implémentation sont dans le même fichier, sinon vous pouvez évidemment simplement mettre les deux fichiers en scène séparément.

  2. Validez le code d'implémentation.

  3. Remontez dans le temps pour tester la validation effectuée à l'étape 1, en vous assurant que le test échoue réellement .

Vous voyez, de cette façon, vous ne comptez pas sur vos prouesses d'édition pour déplacer votre code d'implémentation pendant que vous testez votre test d'échec. Vous utilisez votre VCS pour sauvegarder votre travail et pour vous assurer que l'historique enregistré VCS comprend correctement à la fois l'échec et le test de réussite.

cmaster - réintégrer monica
la source