Dans une autre question, il a été révélé que l'un des problèmes avec TDD est de garder la suite de tests synchronisée avec la base de code pendant et après la refactorisation.
Maintenant, je suis un grand fan de refactoring. Je ne vais pas renoncer à faire du TDD. Mais j'ai également rencontré des problèmes de tests écrits de telle manière qu'une refactorisation mineure entraîne de nombreux échecs de test.
Comment éviter de casser les tests lors de la refactorisation?
- Ecrivez-vous les tests «mieux»? Si oui, que devez-vous rechercher?
- Évitez-vous certains types de refactoring?
- Existe-t-il des outils de refactorisation des tests?
Edit: j'ai écrit une nouvelle question qui demandait ce que je voulais demander (mais j'ai gardé celle-ci comme une variante intéressante).
development-process
tdd
refactoring
Alex Feinman
la source
la source
Réponses:
Ce que vous essayez de faire n'est pas vraiment de refactoring. Avec le refactoring, par définition, vous ne changez pas ce que fait votre logiciel, vous changez comment il le fait.
Commencez par tous les tests verts (tous réussissent), puis apportez des modifications «sous le capot» (par exemple, déplacez une méthode d'une classe dérivée vers la base, extrayez une méthode ou encapsulez un composite avec un constructeur , etc.). Vos tests devraient toujours réussir.
Ce que vous décrivez ne semble pas être une refactorisation, mais une refonte, qui augmente également les fonctionnalités de votre logiciel testé. TDD et refactoring (comme j'ai essayé de le définir ici) ne sont pas en conflit. Vous pouvez toujours refactoriser (vert-vert) et appliquer TDD (rouge-vert) pour développer la fonctionnalité "delta".
la source
L'un des avantages des tests unitaires est de pouvoir refaçonner en toute confiance.
Si le refactoring ne change pas l'interface publique, vous laissez les tests unitaires tels quels et assurez-vous qu'après le refactoring, ils réussissent tous.
Si le refactoring modifie effectivement l'interface publique, les tests doivent d'abord être réécrits. Refactorisez jusqu'à ce que les nouveaux tests réussissent.
Je n'éviterais jamais de refactoring car ça casse les tests. Écrire des tests unitaires peut être une douleur dans un cul, mais cela en vaut la peine à long terme.
la source
Contrairement aux autres réponses, il est important de noter que certaines méthodes de test peuvent devenir fragiles lorsque le système sous test (SUT) est refactorisé, si le test est en boîte blanche.
Si j'utilise un framework de mocking qui vérifie l' ordre des méthodes appelées sur les mocks (lorsque l'ordre n'est pas pertinent car les appels sont sans effet secondaire); puis si mon code est plus propre avec ces appels de méthode dans un ordre différent et que je refactorise, alors mon test se cassera. En général, les simulations peuvent introduire de la fragilité dans les tests.
Si je vérifie l'état interne de mon SUT en exposant ses membres privés ou protégés (nous pourrions utiliser "friend" dans Visual Basic, ou augmenter le niveau d'accès "internal" et utiliser "internalsvisibleto" en c #; dans de nombreuses langues OO, y compris c # une " sous-classe-test-spécifique " pourrait être utilisée), puis soudainement l'état interne de la classe importera - vous pouvez refactoriser la classe en boîte noire, mais les tests en boîte blanche échoueront. Supposons qu'un seul champ soit réutilisé pour signifier différentes choses (ce n'est pas une bonne pratique!) Lorsque le SUT change d'état - si nous le divisons en deux champs, nous devrons peut-être réécrire des tests défectueux.
Les sous-classes spécifiques aux tests peuvent également être utilisées pour tester des méthodes protégées - ce qui peut signifier qu'un refactorisateur du point de vue du code de production est un changement radical du point de vue du code de test. Déplacer quelques lignes dans ou hors d'une méthode protégée peut ne pas avoir d'effets secondaires sur la production, mais casser un test.
Si j'utilise des " hooks de test " ou tout autre code de compilation spécifique au test ou conditionnel, il peut être difficile de s'assurer que les tests ne se cassent pas en raison de fragiles dépendances de la logique interne.
Ainsi, pour éviter que les tests ne se couplent aux détails internes intimes du SUT, cela peut aider à:
Tous les points ci-dessus sont des exemples de couplage en boîte blanche utilisés dans les tests. Donc, pour éviter complètement de refactoriser les tests de rupture, utilisez les tests de boîte noire du SUT.
Avertissement: Pour discuter de la refactorisation ici, j'utilise le mot un peu plus largement pour inclure la modification de l'implémentation interne sans aucun effet externe visible. Certains puristes peuvent être en désaccord et se référer exclusivement au livre Refactoring de Martin Fowler et Kent Beck - qui décrit les opérations de refactoring atomique.
Dans la pratique, nous avons tendance à prendre des étapes incessantes légèrement plus importantes que les opérations atomiques décrites ici, et en particulier les modifications qui laissent le code de production se comporter de manière identique de l'extérieur peuvent ne pas laisser passer les tests. Mais je pense qu'il est juste d'inclure "un algorithme de remplacement pour un autre algorithme qui a un comportement identique" en tant que refactor, et je pense que Fowler est d'accord. Martin Fowler lui-même dit que le refactoring peut casser les tests:
la source
Si vos tests échouent lors de la refactorisation, vous n'êtes pas, par définition, une refactorisation, ce qui "change la structure de votre programme sans changer le comportement de votre programme".
Parfois, vous devez changer le comportement de vos tests. Vous devez peut-être fusionner deux méthodes (par exemple, bind () et listen () sur une classe de socket TCP en écoute), de sorte que d'autres parties de votre code essaient et échouent à utiliser l'API désormais modifiée. Mais ce n'est pas du refactoring!
la source
Je pense que le problème avec cette question, c'est que différentes personnes prennent le mot «refactoring» différemment. Je pense qu'il est préférable de définir soigneusement certaines choses que vous voulez probablement dire:
Comme une autre personne l'a déjà noté, si vous conservez la même API et que tous vos tests de régression fonctionnent sur l'API publique, vous ne devriez avoir aucun problème. La refactorisation ne devrait poser aucun problème. Tout test échoué signifie que votre ancien code avait un bogue et que votre test est mauvais, ou que votre nouveau code a un bogue.
Mais c'est assez évident. Vous entendez donc PROBABLEMENT en refactorisant, que vous modifiez l'API.
Alors laissez-moi vous expliquer comment aborder cela!
Créez d'abord une NOUVELLE API, qui fait ce que vous voulez que votre comportement de NOUVELLE API soit. S'il arrive que cette nouvelle API porte le même nom qu'une ancienne API, j'ajoute le nom _NEW au nouveau nom de l'API.
int DoSomethingInterestingAPI ();
devient:
OK - à ce stade - tous vos tests de régression réussissent - en utilisant le nom DoSomethingInterestingAPI ().
SUIVANT, parcourez votre code et remplacez tous les appels par DoSomethingInterestingAPI () par la variante appropriée de DoSomethingInterestingAPI_NEW (). Cela inclut la mise à jour / réécriture de toutes les parties de vos tests de régression qui doivent être modifiées pour utiliser la nouvelle API.
SUIVANT, marquez DoSomethingInterestingAPI_OLD () comme [[obsolète ()]]. Restez dans l'API obsolète aussi longtemps que vous le souhaitez (jusqu'à ce que vous ayez mis à jour en toute sécurité tout le code qui pourrait en dépendre).
Avec cette approche, les échecs dans vos tests de régression sont simplement des bogues dans ce test de régression ou identifient les bogues dans votre code - exactement comme vous le souhaitez. Ce processus par étapes de révision d'une API en créant explicitement les versions _NEW et _OLD de l'API vous permet de faire coexister des bits du nouveau et de l'ancien code pendant un certain temps.
la source
Je suppose que vos tests unitaires sont d'une granularité que j'appellerais "stupide" :) c'est-à-dire qu'ils testent les minuties absolues de chaque classe et fonction. Éloignez-vous des outils générateurs de code et écrivez des tests qui s'appliquent à une plus grande surface, puis vous pouvez refactoriser les composants internes autant que vous le souhaitez, sachant que les interfaces de vos applications n'ont pas changé et que vos tests fonctionnent toujours.
Si vous voulez avoir des tests unitaires qui testent chaque méthode, alors attendez-vous à devoir les refactoriser en même temps.
la source
Ce qui rend difficile le couplage . Tous les tests sont accompagnés d'un certain degré de couplage avec les détails de l'implémentation, mais les tests unitaires (qu'ils soient TDD ou non) sont particulièrement mauvais car ils interfèrent avec les internes: plus de tests unitaires équivalent à plus de code couplé à des unités, c'est-à-dire des signatures de méthodes / toute autre interface publique d'unités - au moins.
Les «unités» par définition sont des détails d'implémentation de bas niveau, l'interface des unités peut et doit changer / diviser / fusionner et autrement muter à mesure que le système évolue. L'abondance des tests unitaires peut en fait entraver cette évolution plus qu'elle ne contribue.
Comment éviter de casser les tests lors de la refactorisation? Évitez le couplage. En pratique, cela signifie éviter autant de tests unitaires que possible et préférer des tests de niveau supérieur / d'intégration plus agnostiques des détails d'implémentation. N'oubliez pas qu'il n'y a pas de solution miracle, les tests doivent encore être couplés à quelque chose à un certain niveau, mais idéalement, il devrait s'agir d'une interface explicitement versionnée à l'aide de la version sémantique, c'est-à-dire généralement au niveau de l'api / de l'application publiée (vous ne voulez pas faire SemVer pour chaque unité de votre solution).
la source
Vos tests sont trop étroitement liés à la mise en œuvre et non à l'exigence.
pensez à écrire vos tests avec des commentaires comme celui-ci:
de cette façon, vous ne pouvez pas refactoriser le sens des tests.
la source