it-swarm-fr.com

Comment maintenez-vous vos tests d'unité travaillant lors du refactoring?

Dans une autre question, il a été révélé que l'une des douleurs avec TDD maintient la suite de tests en synchronisation avec le codeBase pendant et après le refactoring.

Maintenant, je suis un grand fan de refactoring. Je ne vais pas le donner à TDD. Mais j'ai également connu les problèmes des tests écrits de manière à ce que le refactoring mineur conduit à de nombreux échecs de test.

Comment éviter de casser les tests lorsque vous refactez?

  • Écrivez-vous les tests "mieux"? Si oui, que devriez-vous rechercher?
  • Évitez-vous certains types de refactoring?
  • Y a-t-il des outils de refactoring de test?

Edit: J'ai écrit une nouvelle question qui a demandé ce que je voulais demander (mais gardé celui-ci comme une variante intéressante).

33
Alex Feinman

Ce que vous essayez de faire n'est pas vraiment refactoring. Avec refactoring, par définition, vous ne changez pas quoi Votre logiciel fait, vous changez Comment Cela le fait.

Commencez avec tous les tests verts (tout passe), puis apportez des modifications "sous la hotte" (par exemple, déplacez une méthode à partir d'une classe dérivée à la base, extrayez une méthode ou encapsuler un composite avec un générateur , etc.). Vos tests doivent toujours passer.

Ce que vous décrivez semble ne pas refléter, mais une refonte, qui augmente également les fonctionnalités de votre logiciel à tester. TDD et refactoring (comme j'ai essayé de le définir ici) ne sont pas en conflit. Vous pouvez toujours refacteur (vert-vert) et appliquer TDD (rouge-vert) pour développer la fonctionnalité "Delta".

38
azheglov

L'un des avantages d'avoir des tests d'unités est donc afin que vous puissiez refroidir en toute confiance.

Si le refactoring ne change pas l'interface publique, vous quittez les tests de l'unité comme cela et assurez-vous après avoir refactoring qu'ils passent tous.

Si le refactoring modifie l'interface publique, les tests doivent être réécrites en premier. Refacteur jusqu'à ce que les nouveaux tests passent.

Je n'éviterais jamais de refactoring car il brise les tests. Les tests d'unité d'écriture peuvent être une douleur dans une crosse, mais sa peine vaut la douleur à long terme.

21
Tim Murphy

Contrairement aux autres réponses, il est important de noter que certaines méthodes de test peuvent deviennent fragiles lorsque le système sous test (SUT) est refactored Si Le test est BlancBox.

Si j'utilise un cadre moqueur qui vérifie l'ordre des méthodes appelées sur les moqueurs (lorsque la commande n'est pas pertinente, car les appels sont effets secondaires libre); Ensuite, si mon code est plus propre avec ces appels de méthode dans un ordre différent et que je refacteur, mon test se brisera. En général, les moqueurs peuvent introduire une fragilité aux tests.

Si je vérifie l'état interne de ma SUT en exposant ses membres privés ou protégés (nous pourrions utiliser "ami" dans Visual Basic, ou escalader le niveau d'accès "interne" et utiliser "InternalsVissibleto" dans C #; dans de nombreux OO Langues, y compris C # A " Sous-classe spécifique au test " pourrait être utilisé) alors soudainement l'état interne de la classe importera - vous pouvez refléter la classe comme une boîte noire , mais les tests de boîte blanche échoueront. Supposons qu'un seul champ soit réutilisé pour signifier différentes choses (pas de bonnes pratiques!) Lorsque la sète change d'état - si nous le divisions en deux champs, nous devrons peut-être réécrire les tests brisés.

Les sous-classes spécifiques au test peuvent également être utilisées pour tester des méthodes protégées - ce qui peut signifier qu'un refacteur du point de vue du code de production est un changement de rupture du point de vue du code de test. Déplacer quelques lignes dans ou hors d'une méthode protégée ne peut avoir aucun effet secondaire de production, mais briser un test.

Si j'utilise " Crochets de test " ou tout autre code de compilation spécifique au test ou conditionnel, il peut être difficile de garantir que les tests ne cassent pas à cause de dépendances fragiles sur la logique interne.

Afin d'empêcher les tests de devenir couplés aux détails internes intimes du SUT, cela peut aider à:

  • Utilisez des talons plutôt que des simulacres, dans la mesure du possible. Pour plus d'informations, voir Blog de Fabio Periera sur les tests tautologiques , et mon blog sur les tests tautologiques .
  • Si vous utilisez des simulacres, évitez de vérifier l'ordre des méthodes appelées, à moins que cela ne soit important.
  • Essayez d'éviter de vérifier l'état interne de votre SUT - utilisez son API externe si possible.
  • Essayez d'éviter la logique spécifique au test dans le code de production
  • Essayez d'éviter d'utiliser des sous-classes spécifiques à des tests.

Tous les points ci-dessus sont des exemples de couplage de boîte blanche utilisée dans les tests. Donc, pour éviter complètement les tests de rupture de refactoring, utilisez des tests en boîte noire de la SUT.

Disclaimer: Dans le but de discuter de refactoring ici, j'utilise le mot un peu plus largement pour inclure la mise en œuvre interne modifiée sans effets externes visibles. Certains puristes peuvent être en désaccord et se référer exclusivement à Martin Fowler et au refactoring du livre de Kent Beck - qui décrit les opérations de refactorisation atomique.

En pratique, nous avons tendance à prendre des étapes non rompues légèrement plus grandes que les opérations atomiques décrites là-bas et, en particulier, les changements laissant le code de production se comporter de manière identique à l'extérieur peuvent ne pas laisser de tests passant. Mais je pense qu'il est juste d'inclure "l'algorithme de substitution d'un autre algorithme ayant un comportement identique" comme refacteur, et je pense que Fowler est d'accord. Martin Fowler lui-même dit que le refactoring peut casser des tests:

Lorsque vous écrivez un test de moqueur, vous testez les appels sortants du SUT pour vous assurer qu'il parle correctement à ses fournisseurs. Un test classique ne se soucie que de l'état final - et non de la manière dont cet état a été dérivé. Des tests de mastication sont donc plus couplés à la mise en oeuvre d'une méthode. Changer la nature des appels vers des collaborateurs entraînent généralement une pause d'un test ma moqueur.

[...]

Le couplage à la mise en œuvre interfère également avec le refactoring, car les changements de mise en œuvre sont beaucoup plus susceptibles de casser des tests que d'essais classiques.

Fowler - Mocks n'est pas des stubs

10
perfectionist

Si vos tests se cassent lorsque vous refactez, vous n'êtes pas, par définition, refactoring, ce qui "modifie la structure de votre programme sans changer le comportement de votre programme".

Parfois, vous devez changer le comportement de vos tests. Peut-être avez-vous besoin de fusionner deux méthodes ensemble (disons, bind () et écouter () sur une classe d'écoute TCP), vous disposez donc d'autres parties de votre code et d'avoir omis de l'utiliser maintenant. API altéré. Mais cela ne refactore pas!

5
Frank Shearar

Je pense que les problèmes 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 avec soin quelques choses que vous voulez probablement dire:

>  Keep the API the same, but change how the API is implemented internally
>  Change the API

Comme une autre personne déjà notée, si vous conservez l'API de la même manière et que tous vos tests de régression fonctionnent sur l'API publique, vous ne devriez avoir aucun problème. Le refactoring ne devrait causer aucun problème. Tous les tests échoués signifient que votre ancien code avaient un bogue et votre test est mauvais, ou votre nouveau code a un bogue.

Mais c'est assez évident. Donc, vous entendez probablement en refactorisant que vous changez l'API.

Alors laissez-moi répondre à comment approcher cela!

  • Créez d'abord une nouvelle API, cela fait ce que vous voulez que votre nouveau comportement de l'API soit. S'il arrive que cette nouvelle API ait le même nom qu'une API plus ancienne, j'appuie le nom _NEW au nouveau nom de l'API.

    int DosomethingIntIserSingAstingAnimi ();

devient:

int DoSomethingInterestingAPI_NEW( int takes_more_arguments );
int DoSomethingInterestingAPI_OLD();
int DoSomethingInterestingAPI() { DoSomethingInterestingAPI_NEW (whatever_default_mimics_the_old_API);

OK - à ce stade - tous vos tests de régression SILL PASS - en utilisant le nom DosomethingIntionStingAstingApi ().

Ensuite, passez à travers votre code et modifiez tous les appels vers DosomethingestingAnimi () à la variante appropriée de DosomethingSingAstingAnIn_New (). Cela inclut la mise à jour/la réécriture des pièces de vos tests de régression doit être modifiée pour utiliser la nouvelle API.

Ensuite, marquez DosomethingIntingAstingAnth_oletHolething () comme [[obsolète ()]]. Gardez autour de 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, toute défaillance dans vos tests de régression est simplement des bogues dans cet essai de régression ou identifier des bugs dans votre code - exactement comme vous le voudriez. Ce processus mis en scène de révision d'une API en créant explicitement _NEW et les versions _old de l'API vous permettent d'avoir des bits du nouveau et de l'ancien code coexistant pendant un moment.

4
Lewis Pringle

garder la suite de tests en synchronisation avec le codeBase pendant et après refactoring

Ce qui rend difficile, c'est couplage. Tous les tests viennent avec un certain degré d'accouplement aux détails de la mise en œuvre, mais des tests unitaires (indépendamment de la TDD ou non) sont particulièrement mauvais, car ils interfèrent avec des internaux: plus de tests d'unités est égal à plus de code couplé aux unités, c'est-à-dire des méthodes Signatures/toute autre interface publique des unités - au moins.

Les "unités" par définition sont des détails de mise en œuvre de bas niveau, l'interface d'unités peut et doit modifier/diviser/fusionner et sinon la mutation car le système évolue. L'abondance des tests unitaires peut réellement entraver cette évolution plus que nécessaire.

Comment éviter de casser des tests lors du refactoring? Éviter les couples. En pratique, cela signifie éviter autant de tests de l'unité que possible et préférez des tests de niveau/d'intégration plus élevés plus agnostiques des détails de la mise en œuvre. N'oubliez pas qu'il n'y ait pas de balle d'argent, les tests doivent encore coupler à quelque chose à un certain niveau, mais il convient parfaitement à une interface qui est explicitement versée à l'aide de la version sémantique, c'est-à-dire généralement au niveau de l'API/application publié (vous ne voulez pas faire Semver pour chaque unité de votre solution).

1
KolA

Je suppose que vos tests d'unité sont d'une granularité que j'appellerais "stupide" :) c'est-à-dire qu'ils testent la minutie absolue de chaque classe et de chaque fonction. Éloignez-vous des outils de générateur de code et des tests d'écriture qui s'appliquent à une plus grande surface, vous pouvez refroidir autant que vous le souhaitez, sachant que les interfaces à vos applications n'ont pas changé et que vos tests fonctionnent toujours.

Si vous souhaitez avoir des tests d'unités qui testent chaque méthode, attendez-vous à avoir à les refracter en même temps.

1
gbjbaanb

Vos tests sont trop étroitement couplés à la mise en œuvre et non à l'exigence.

pensez à écrire vos tests avec des commentaires comme celui-ci:

//given something
...test code...
//and something else
...test code...
//when something happens
...test code...
//then the state should be...
...test code...

de cette façon, vous ne pouvez pas refroidir le sens des tests.

0
mcintyre321