Qu'est-ce qui aiderait à refactoriser une grande méthode pour m'assurer que je ne casse rien?

10

Je suis actuellement en train de refactoriser une partie d'une grande base de code sans aucun test unitaire. J'ai essayé de refactoriser le code de manière brute, c'est-à-dire en essayant de deviner ce que fait le code et quels changements ne changeraient pas sa signification, mais sans succès: il casse au hasard les fonctionnalités tout autour de la base de code.

Notez que le refactoring comprend le déplacement du code C # hérité vers un style plus fonctionnel (le code hérité n'utilise aucune des fonctionnalités de .NET Framework 3 et versions ultérieures, y compris LINQ), l'ajout de génériques là où le code peut en bénéficier, etc.

Je ne peux pas utiliser de méthodes formelles , étant donné combien coûteraient-elles.

D'un autre côté, je suppose qu'au moins la règle "Tout code hérité refacturé doit être accompagné de tests unitaires" doit être strictement suivie, quel qu'en soit le coût. Le problème est que lorsque je refaçonne une petite partie d'une méthode privée de 500 LOC, l'ajout de tests unitaires semble être une tâche difficile.

Qu'est-ce qui peut m'aider à savoir quels tests unitaires sont pertinents pour un morceau de code donné? Je suppose que l'analyse statique du code serait en quelque sorte utile, mais quels sont les outils et les techniques que je peux utiliser pour:

  • Savoir exactement quels tests unitaires dois-je créer,

  • Et / ou savez-vous si le changement que j'ai effectué a affecté le code d'origine d'une manière qu'il s'exécute différemment à partir de maintenant?

Arseni Mourzenko
la source
Quel est votre raisonnement selon lequel l'écriture de tests unitaires augmentera la durée de ce projet? De nombreux partisans seraient en désaccord, mais cela dépend aussi de votre capacité à les écrire.
JeffO
Je ne dis pas que cela augmentera la durée globale du projet. Ce que je voulais dire, c'est que cela augmentera le temps à court terme (c'est-à-dire le temps immédiat que je passe en ce moment lors de la refactorisation du code).
Arseni Mourzenko
1
Vous ne voudriez pas utiliser de formal methods in software developmenttoute façon, car il est utilisé pour prouver l'exactitude d'un programme en utilisant une logique de prédicat et n'aurait pas d'application pour refactoriser une grande base de code. Méthodes formelles généralement utilisées pour prouver que le code fonctionne correctement dans des domaines tels que les applications médicales. Vous avez raison, il est coûteux de le faire, c'est pourquoi il n'est pas utilisé souvent.
Mushy
Un bon outil tel que les options de refactorisation dans ReSharper rendent cette tâche beaucoup plus facile. Dans de telles situations, cela en vaut la peine .
billy.bob
1
Pas une réponse complète, mais une technique stupide que je trouve étonnamment efficace lorsque toutes les autres techniques de refactoring échouent: créez une nouvelle classe, divisez la fonction en fonctions séparées avec exactement le code déjà là, juste cassé toutes les 50 lignes environ, promouvez n'importe quelle les sections locales qui sont partagées entre les fonctions aux membres, puis les fonctions individuelles s'inscrivent mieux dans ma tête et me donnent la possibilité de voir dans les membres quelles pièces sont enfilées dans toute la logique. Ce n'est pas un objectif final, juste un moyen sûr de préparer un désordre hérité pour qu'il soit prêt pour une refactorisation en toute sécurité.
Jimmy Hoffa

Réponses:

12

J'ai eu des défis similaires. Le livre Working with Legacy Code est une excellente ressource, mais il y a une supposition que vous pouvez faire du klaxon lors de tests unitaires pour soutenir votre travail. Parfois, ce n'est tout simplement pas possible.

Dans mon travail d'archéologie (mon terme pour la maintenance d'un code hérité comme celui-ci), je suis une approche similaire à ce que vous avez décrit.

  • Commencez par une solide compréhension de ce que la routine fait actuellement.
  • En même temps, identifiez ce que la routine était censée faire. Beaucoup pensent que cette balle et la précédente sont les mêmes, mais il y a une différence subtile. Souvent, si la routine faisait ce qu'elle était censée faire, vous n'appliqueriez pas de modifications de maintenance.
  • Exécutez quelques exemples à travers la routine et assurez-vous d'atteindre les cas limites, les chemins d'erreur pertinents, ainsi que le chemin de la ligne principale. D'après mon expérience, les dommages collatéraux (rupture de fonctionnalité) proviennent de conditions aux limites qui ne sont pas mises en œuvre exactement de la même manière.
  • Après ces exemples de cas, identifiez ce qui persiste et qui ne doit pas nécessairement l'être. Encore une fois, j'ai constaté que ce sont des effets secondaires comme celui-ci qui conduisent à des dommages collatéraux ailleurs.

À ce stade, vous devriez avoir une liste de candidats de ce qui a été exposé et / ou manipulé par cette routine. Certaines de ces manipulations sont susceptibles d'être involontaires. Maintenant, j'utilise findstret l'IDE pour comprendre quels autres domaines peuvent référencer les éléments de la liste des candidats. Je vais passer un peu de temps à comprendre comment ces références fonctionnent et quelle est leur nature.

Enfin, une fois que je me suis trompé en pensant que je comprends les impacts de la routine d'origine, je vais effectuer mes modifications une à la fois et réexécuter les étapes d'analyse que j'ai décrites ci-dessus pour vérifier que le changement fonctionne comme prévu. ça marche. J'essaie spécifiquement d'éviter de changer plusieurs choses à la fois car j'ai trouvé que cela explose sur moi lorsque j'essaie de vérifier l'impact. Parfois, vous pouvez vous en sortir avec plusieurs changements, mais si je peux suivre un itinéraire un par un, c'est ma préférence.

Bref, mon approche est similaire à celle que vous avez exposée. C'est beaucoup de travail de préparation; puis faites des changements individuels circonspects; puis vérifiez, vérifiez, vérifiez.


la source
2
+1 pour l'utilisation de "l'archéologie" uniquement. C'est le même terme que j'utilise pour décrire cette activité et je pense que c'est une excellente façon de le dire (également pensé que la réponse était bonne - je ne suis pas vraiment superficiel)
Erik Dietrich
10

Qu'est-ce qui aiderait à refactoriser une grande méthode pour m'assurer que je ne casse rien?

Réponse courte: petits pas.

Le problème est que lorsque je refaçonne une petite partie d'une méthode privée de 500 LOC, l'ajout de tests unitaires semble être une tâche difficile.

Considérez ces étapes:

  1. Déplacez l'implémentation dans une fonction différente (privée) et déléguez l'appel.

    // old:
    private int ugly500loc(int parameters) {
        // 500 LOC here
    }
    
    // new:    
    private int ugly500loc_old(int parameters) {
        // 500 LOC here
    }
    
    private void ugly500loc(int parameters) {
        return ugly500loc_old(parameters);
    }
    
  2. Ajoutez le code de journalisation (assurez-vous que la journalisation n'échoue pas) dans votre fonction d'origine, pour toutes les entrées et sorties.

    private void ugly500loc(int parameters) {
        static int call_count = 0;
        int current = ++call_count;
        save_to_file(current, parameters);
        int result = ugly500loc_old(parameters);
        save_to_file(current, result); // result, any exceptions, etc.
        return result;
    }
    

    Exécutez votre application et faites tout ce que vous pouvez avec elle (utilisation valide, utilisation non valide, utilisation typique, utilisation atypique, etc.).

  3. Vous avez maintenant des max(call_count)ensembles d'entrées et de sorties avec lesquels écrire vos tests; Vous pouvez écrire un seul test qui itère sur tous vos paramètres / jeux de résultats que vous avez et les exécute en boucle. Vous pouvez également écrire un test supplémentaire qui exécute une combinaison particulière (à utiliser pour vérifier rapidement le passage sur un ensemble d'E / S particulier).

  4. Déplacer de // 500 LOC herenouveau dans votre ugly500locfonction (et supprimer une fonction de journalisation).

  5. Commencez à extraire les fonctions de la grande fonction (ne faites rien d'autre, extrayez simplement les fonctions) et exécutez les tests. Après cela, vous devriez avoir plus de petites fonctions à refactoriser, au lieu de 500LOC.

  6. Vivre heureux pour toujours.

utnapistim
la source
3

Habituellement, les tests unitaires sont la voie à suivre.

Faites les tests nécessaires qui prouvent que le courant fonctionne comme prévu. Prenez votre temps et le dernier test doit vous faire confiance sur la sortie.

Qu'est-ce qui peut m'aider à savoir quels tests unitaires sont pertinents pour un morceau de code donné?

Vous êtes en train de refactoriser un morceau de code, vous devez savoir exactement ce qu'il fait et ce qu'il impacte. Donc, fondamentalement, vous devez tester toutes les zones touchées. Cela vous prendra beaucoup de temps ... mais c'est le résultat attendu de tout processus de refactoring.

Ensuite, vous pouvez tout déchirer sans aucun problème.

AFAIK, il n'y a pas de technique pare-balles pour cela ... il vous suffit d'être méthodique (quelle que soit la méthode sur laquelle vous vous sentez à l'aise), beaucoup de temps et beaucoup de patience! :)

Bravo et bonne chance!

Alex

AlexCode
la source
Les outils de couverture de code sont essentiels ici. Il est difficile de confirmer que vous avez parcouru chaque chemin à travers une grande méthode complexe via l'inspection. Un outil qui montre que, collectivement, KitchenSinkMethodTest01 () ... KitchenSinkMethodTest17 () couvre les lignes 1-45, 48-220, 245-399 et 488-500 mais ne touche pas le code entre; rendra beaucoup plus simple de déterminer les tests supplémentaires dont vous avez besoin pour écrire.
Dan est