Je refactorise une énorme classe de code héritée. Refactoring (je présume) préconise ceci:
- écrire des tests pour la classe héritée
- refactoriser le diable hors de la classe
Problème: une fois que j'ai refactorisé la classe, mes tests de l'étape 1 devront être modifiés. Par exemple, ce qui était autrefois dans une méthode héritée, peut désormais être une classe distincte à la place. Ce qui était une méthode peut être maintenant plusieurs méthodes. Le paysage entier de la classe héritée peut être effacé en quelque chose de nouveau, et donc les tests que j'écris à l'étape 1 seront presque nuls et non avenus. Essentiellement, j'ajouterai l' étape 3. réécris abondamment mes tests
A quoi bon alors écrire des tests avant refactoriser? Cela ressemble plus à un exercice académique de création de plus de travail pour moi. J'écris des tests pour la méthode maintenant et j'apprends plus sur comment tester les choses et comment la méthode héritée fonctionne. On peut l'apprendre en lisant simplement le code hérité lui-même, mais écrire des tests revient presque à me frotter le nez et à documenter cette connaissance temporaire dans des tests séparés. Donc, de cette façon, je n'ai presque pas d'autre choix que d'apprendre ce que fait le code. J'ai dit temporaire ici, car je vais refactoriser le diable du code et toute ma documentation et mes tests seront nuls pour une grande partie, sauf que mes connaissances resteront et me permettront d'être plus frais sur le refactoring.
Est-ce la vraie raison d'écrire des tests avant refactor - pour m'aider à mieux comprendre le code? Il doit y avoir une autre raison!
S'il vous plaît, expliquez!
Remarque:
Il y a ce post: Est-il judicieux d'écrire des tests pour le code hérité lorsqu'il n'y a pas de temps pour une refactorisation complète? mais il dit "écrire des tests avant refactoriser", mais ne dit pas "pourquoi", ni quoi faire si "écrire des tests" semble "un travail occupé qui sera bientôt détruit"
la source
Réponses:
La refactorisation consiste à nettoyer un morceau de code (par exemple, améliorer le style, la conception ou les algorithmes), sans modifier le comportement (visible de l'extérieur). Vous écrivez des tests pour ne pas vous assurer que le code avant et après refactoring est le même, mais vous écrivez des tests comme indicateur que votre application avant et après refactoring se comporte de la même manière: le nouveau code est compatible et aucun nouveau bogue n'a été introduit.
Votre principale préoccupation devrait être d'écrire des tests unitaires pour l'interface publique de votre logiciel. Cette interface ne devrait pas changer, donc les tests (qui sont une vérification automatisée de cette interface) ne devraient pas non plus changer.
Cependant, les tests sont également utiles pour localiser les erreurs, il peut donc être judicieux d'écrire des tests pour les parties privées de votre logiciel. Ces tests devraient changer tout au long du refactoring. Si vous souhaitez modifier un détail d'implémentation (comme le nom d'une fonction privée), vous mettez d'abord à jour les tests pour refléter vos attentes modifiées, puis assurez-vous que le test échoue (vos attentes ne sont pas satisfaites), puis vous changez le code réel et vérifiez que tous les tests réussissent à nouveau. À aucun moment, les tests de l'interface publique ne doivent échouer.
Cela est plus difficile à réaliser lors de modifications à plus grande échelle, par exemple en repensant plusieurs pièces codépendantes. Mais il y aura une sorte de limite, et à cette limite, vous pourrez écrire des tests.
la source
Ah, maintenir les anciens systèmes.
Idéalement, vos tests traitent la classe uniquement via son interface avec le reste de la base de code, d'autres systèmes et / ou l'interface utilisateur. Interfaces. Vous ne pouvez pas refactoriser l'interface sans affecter ces composants en amont ou en aval. Si tout cela est un désordre étroitement couplé, vous pourriez aussi bien considérer l'effort comme une réécriture plutôt que une refactorisation, mais c'est en grande partie de la sémantique.
Edit: Disons qu'une partie de votre code mesure quelque chose et qu'il a une fonction qui renvoie simplement une valeur. La seule interface appelle la fonction / méthode / ainsi de suite et reçoit la valeur retournée. Il s'agit d'un couplage lâche et d'un test facile à l'unité. Si votre programme principal a un sous-composant qui gère un tampon, et tous les appels à celui-ci dépendent du tampon lui-même, de certaines variables de contrôle, et il repousse les messages d'erreur via une autre section de code, alors vous pourriez dire que c'est étroitement couplé et c'est test difficile à réaliser. Vous pouvez toujours le faire avec des quantités suffisantes d'objets fictifs et ainsi de suite, mais cela devient désordonné. Surtout en c. Toute quantité de refactorisation du fonctionnement du tampon cassera le sous-composant.
Fin de la modification
Si vous testez votre classe via des interfaces qui restent stables, vos tests doivent être valides avant et après le refactoring. Cela vous permet d'apporter des modifications en toute confiance que vous ne l'avez pas cassé. Au moins, plus de confiance.
Il vous permet également d'effectuer des modifications incrémentielles. Si c'est un gros projet, je ne pense pas que vous allez vouloir tout démolir, construire un tout nouveau système puis commencer à développer des tests. Vous pouvez en modifier une partie, la tester et vous assurer que cette modification n'entraîne pas le reste du système. Ou si c'est le cas, vous pouvez au moins voir le désordre géant emmêlé se développer plutôt que d'être surpris par lui lorsque vous relâchez.
Bien que vous puissiez diviser une méthode en trois, ils feront toujours la même chose que la méthode précédente, vous pouvez donc passer le test de l'ancienne méthode et la diviser en trois. L'effort d'écriture du premier test n'est pas perdu.
De plus, traiter la connaissance du système hérité comme une «connaissance temporaire» ne se fera pas bien. Savoir comment cela fonctionnait auparavant est essentiel pour les systèmes hérités. Vraiment utile pour la question séculaire de "pourquoi diable fait-il cela?"
la source
Ma propre réponse / réalisation:
De la correction de diverses erreurs lors de la refactorisation, je me rends compte que je n'aurais pas fait les mouvements de code aussi facilement sans avoir de tests. Les tests m'informent des "différences" comportementales / fonctionnelles que j'introduis en changeant mon code.
Vous n'avez pas besoin d'être hyper conscient lorsque vous avez de bons tests en place. Vous pouvez modifier votre code dans un comportement plus détendu. Les tests font les vérifications et les vérifications d'intégrité pour vous.
De plus, mes tests sont restés à peu près les mêmes que ceux que j'ai refactorisés et n'ont pas été détruits. J'ai en fait remarqué des opportunités supplémentaires pour ajouter des assertions à mes tests en approfondissant le code.
MISE À JOUR
Eh bien, maintenant je change beaucoup mes tests: / Parce que j'ai refactorisé la fonction d'origine (supprimé la fonction et créé une nouvelle classe plus propre à la place, en déplaçant le fluff qui était à l'intérieur de la fonction en dehors de la nouvelle classe), alors maintenant le code en cours de test que j'ai exécuté auparavant prend différents paramètres sous un nom de classe différent et produit des résultats différents (le code original avec le fluff avait plus de résultats à tester). Et donc mes tests doivent refléter ces changements et, fondamentalement, je réécris mes tests en quelque chose de nouveau.
Je suppose qu'il y a d'autres solutions que je peux faire pour éviter de réécrire les tests. c'est-à-dire conserver l'ancien nom de la fonction avec le nouveau code et le contenu à l'intérieur ... mais je ne sais pas si c'est la meilleure idée et je n'ai pas encore beaucoup d'expérience pour faire un jugement sur ce qu'il faut faire.
la source
Utilisez vos tests pour piloter votre code pendant que vous le faites. Dans le code hérité, cela signifie écrire des tests pour le code que vous allez modifier. De cette façon, ils ne sont pas un artefact séparé. Les tests doivent porter sur ce que le code doit réaliser et non sur les entrailles de la façon dont il le fait.
Généralement, vous voulez ajouter des tests sur du code qui n'en a aucun) pour le code que vous allez refactoriser pour vous assurer que le comportement des codes continue de fonctionner comme prévu. Ainsi, l'exécution continue de la suite de tests tout en refactorisant est un filet de sécurité fantastique. L'idée de changer le code sans suite de tests pour confirmer que les changements n'affectent pas quelque chose d'imprévu est effrayante.
Pour ce qui est du détail de la mise à jour d'anciens tests, de l'écriture de nouveaux tests, de la suppression d'anciens tests, etc.
la source
Quel est l'objectif de refactoring dans votre cas spécifique?
Supposons, pour les besoins de ma réponse, que nous croyons tous (dans une certaine mesure) au TDD (Test-Driven Development).
Si le but de votre refactoring est de nettoyer le code existant sans changer le comportement existant, alors l'écriture de tests avant le refactoring est la façon dont vous vous assurez que vous n'avez pas changé le comportement du code, si vous réussissez, alors les tests réussiront avant et après vous refactorisez.
Les tests vous aideront à vous assurer que votre nouveau travail fonctionne réellement.
Les tests découvriront probablement également des cas où l'œuvre originale ne fonctionne pas .
Mais comment faites-vous vraiment une refactorisation importante sans affecter le comportement dans une certaine mesure?
Voici une courte liste de quelques choses qui pourraient se produire lors de la refactorisation:
Je vais faire valoir que chacune de ces activités énumérées change le comportement d'une manière ou d'une autre.
Et je vais faire valoir que si votre refactoring change de comportement, vos tests vont toujours être la façon dont vous vous assurez que vous n'avez rien cassé.
Peut-être que le comportement ne change pas au niveau macro, mais le point de test unitaire n'est pas d'assurer le comportement macro. Ce sont des tests d' intégration . Le but des tests unitaires est de s'assurer que les morceaux individuels à partir desquels vous construisez votre produit ne sont pas cassés. Chaîne, maillon le plus faible, etc.
Que diriez-vous de ce scénario:
Supposons que vous ayez
function bar()
function foo()
fait un appel àbar()
function flee()
effectue également un appel à la fonctionbar()
Juste pour la variété,
flam()
fait un appel àfoo()
Tout fonctionne à merveille (apparemment, au moins).
Vous refactorisez ...
bar()
est renommé enbarista()
flee()
est changé pour appelerbarista()
foo()
n'est pas changé pour appelerbarista()
De toute évidence, vos tests pour les deux
foo()
etflam()
maintenant échouent.Peut - être que vous ne réalisais pas
foo()
appelébar()
en premier lieu. Vous ne saviez certainement pas que celaflam()
dépendaitbar()
defoo()
.Peu importe. Le fait est que vos tests permettront de découvrir le comportement nouvellement rompu des deux
foo()
etflam()
, de manière incrémentielle lors de votre travail de refactoring.Les tests finissent par vous aider à bien refactoriser.
Sauf si vous n'avez aucun test.
C'est un peu un exemple artificiel. Il y a ceux qui diraient que si changer les
bar()
pausesfoo()
, alorsfoo()
c'était trop complexe au départ et devrait être décomposé. Mais les procédures peuvent appeler d'autres procédures pour une raison et il est impossible d'éliminer toute complexité, non? Notre travail consiste à gérer raisonnablement bien la complexité.Prenons un autre scénario.
Vous construisez un bâtiment.
Vous construisez un échafaudage pour vous assurer que le bâtiment est construit correctement.
L'échafaudage vous aide, entre autres, à construire une cage d'ascenseur. Ensuite, vous démontez l'échafaudage, mais la cage d'ascenseur reste. Vous avez détruit "l'œuvre originale" en détruisant l'échafaudage.
L'analogie est ténue, mais le fait est qu'il n'est pas rare de créer des outils pour vous aider à créer un produit. Même si les outils ne sont pas permanents, ils sont utiles (voire nécessaires). Les charpentiers fabriquent des gabarits tout le temps, parfois uniquement pour un seul travail. Ensuite, ils déchirent les gabarits, utilisant parfois les pièces pour construire d'autres gabarits pour d'autres travaux, parfois non. Mais cela ne rend pas les gabarits inutiles ou inutiles.
la source