web-dev-qa-db-fra.com

Comment structurer les engagements lorsque le test unitaire nécessite un refactoring

J'essaie d'obtenir un avis pour mes listes de pros/Infers sur la manière de structurer les engagements qui sont sortis d'une discussion à mon travail.

Voici le scénario:

  • Je dois ajouter une fonctionnalité x à une base de code héritée
  • La base de code actuelle a quelque chose que je ne peux pas vous moquer de la fonctionnalité de test de l'unité x impossible
  • Je peux refracteur pour effectuer des tests unitaires possibles, mais il en résulte un très grand changement de code touchant de nombreuses autres classes non-test qui ont peu en commun avec la fonctionnalité X

Ma société dispose des règles suivantes strictement forcées:

  • Chaque commit doit être autonome (compile, passer des tests, etc.) Nous avons une automatisation qui empêche de fusionner jusqu'à ce que ceux-ci se soient prouvés.
  • Seules les fantassies rapides sont autorisées (aucune branche, aucune fusion ne s'engage, notre référentiel d'origine n'a qu'une seule branche principale et c'est une ligne parfaitement droite)

La question est donc de savoir comment structurer les engagements pour ces 3 choses. (Refactoring, caractéristique X et test de la fonctionnalité X) Mon collègue m'a référé à cet autre article mais il ne semble pas aborder la partie refactorisée. (Je suis d'accord sans la source de refactorisation et le test devraient être dans un commettre) L'article parle de "Breaker Git Bisect" et "en veillant à ce que chaque commit compile/passe "mais nos règles strictes couvrent déjà cela. L'autre autre argument qu'ils donnent est le "code logiquement connexe maintenu ensemble" qui semble un peu philosophique pour moi.

Je vois 3 façons de procéder. J'espère que vous pouvez soit a) ajouter à lui b) commentez pourquoi l'un des pro/contre existants n'est pas important et doit être supprimé de la liste.

Méthode 1 (un commit): Comprend une fonctionnalité X, Test pour la fonctionnalité X et Refactoring

avantages:

  • "Code logiquement associé conservé ensemble" (pas sûr que ce soit en réalité une "raison". Je dirais probablement que toutes les 3 méthodes font cela, mais certains peuvent discuter autrement. Cependant, personne ne peut discuter ici).
  • Si vous chéris-choisissez/revenez sans conflit de conflit, il va probablement toujours compiler et passer des tests
  • Il n'y a jamais de code non couvert par test

les inconvénients:

  • Examen plus difficile à coder. (Pourquoi tout ce refactoring est-il fait ici malgré non être lié à la fonctionnalité X?)
  • Vous ne pouvez pas chier-choisir sans le refactoring. (Vous devez apporter le refactoring, une chance croissante de conflit et du temps passé)

Méthode 2 (deux commits): une inclut la fonction X, puis deux inclut le refactoring et le test pour la fonctionnalité X

avantages:

  • Plus facile à évaluer la révision des deux. (Le refactoring fait uniquement pour des tests est maintenu avec le test qu'il est associé)
  • Vous pouvez chercher des cerises juste la fonctionnalité. (par ex. pour des expériences ou une fonctionnalité d'ajout aux anciennes versions)
  • Si vous décidez de rétablir la fonctionnalité, vous pouvez conserver le code mieux structuré (espérons-le), qui est venu de la refactoring (toutefois, ne sera pas "pur". Voir le contre-ci ci-dessous)

les inconvénients:

  • Il y aura un commit sans couverture de test (même s'il est ajouté immédiatement après, philosophiquement mauvais?)
  • Avoir un commit sans couverture de test permet une application de couverture automatisée durement/impossible pour chaque commit (par exemple, vous avez besoin de la couverture de Y% pour fusionner)
  • Si vous cherchez uniquement le test, cela échouera.
  • Ajoute une charge aux personnes qui souhaitent se rétablir. (Ils avaient besoin de savoir soit de pouvoir revenir à les deux commettrations ou de supprimer le test dans le cadre de la fonctionnalité revenir à la netteter pas "pure")

Méthode 3 (deux commits): une inclut le refactoring, deux inclut la fonction X et Test pour la fonctionnalité X

avantages:

  • Plus facile à évaluer l'examen du deuxième commit. (Le refactoring fait uniquement pour des raisons de test est conservé hors de fonctionnalités;
  • Si vous chéris-choisissez/revenez sans fusion de conflit, il devrait compiler et passer des tests
  • Il n'y a jamais de code non couvert par test (à la fois philosophiquement bien et aussi plus facile pour l'application de la couverture automatisée)

les inconvénients:

  • Plus difficile à coder examiner le premier commit. (Si la seule valeur du refactoring est à tester et que le test est dans un futur commettre, vous devez aller de retour entre les deux pour comprendre pourquoi cela a été fait et si cela aurait pu être mieux fait.)
    • Sans doute le pire des 3 pour "code logiquement connexe maintenu ensemble" (mais probablement pas si important ???)

Donc, en fonction de tout cela, je suis penché vers 3. Avoir la couverture de test automatisée est une grosse victoire (et ce qui m'a démarré dans ce trou de lapin en premier lieu). Mais peut-être que l'un de vous a des avantages/inconvénients que j'ai manqués? Ou peut-être qu'il y a une 4ème options?

25
Paul Nogas

Lorsque vous travaillez sur le code existant, il est courant que vous devez refactoriser le code avant de pouvoir implémenter votre fonctionnalité.

Ceci est le mantra de Kent Beck: "Effectuez le changement facile (AVERTISSEMENT: Cela peut être difficile), puis effectuez le changement facile"

Pour ce faire, je recommande généralement de faire des petits commits fréquents. Prenez des petits pas. Refacteur progressivement:

multiple many commits when working on Legacy Code

Chaque refactoring ne change pas la manière dont le code fonctionne, mais comment cela est mis en œuvre. Ce n'est pas "difficile à examiner" car les deux implémentations sont également valables. Mais la nouvelle mise en œuvre facilitera le changement de changement.

Enfin, écrivez le test et faites-le passer. Il devrait être relativement court et au point. Cela facilite également la lecture de la société.

donc j'irais aussi pour la 3ème option. Peut-être que j'aurais même plusieurs commits de refactorisation. Ou je les écrases dans l'un avant de pousser cela pour examen, donc il n'y en a qu'un. Ou peut-être que je ferais un premier PR qui ne fait que refactoring, puis une seconde qui n'est que la fonctionnalité. Cela dépend vraiment de la quantité de refactoring (garder votre PRS Short) et vos conventions de votre équipe!

Si la seule valeur du refactoring est à tester et que le test est dans un futur commettre, vous devez aller de retour entre les deux pour comprendre pourquoi cela a été fait et si cela aurait pu être fait mieux

Pour résoudre ce problème, vous devez obtenir votre équipe à l'aise dans cette approche: le refacteur d'abord, puis implémentez la fonctionnalité.

Je vous suggérerais de discuter avec vos collègues et de l'essayer. Je vous recommanderais également d'essayer de Pratiquez "Over-commettant" pour vous permettre de faire des commentaires plus petits. C'est une compétence utile pour avoir lorsque le code est délicat, c'est donc un excellent exercice à faire lorsque le code n'est pas!

En tout cas, je pense que vous avez une discussion en bonne santé avec vos collègues. Vous trouverez sans doute ce qui fonctionne pour votre équipe!

34
nicoespeon

Allez avec 3 - mais mentionnez la raison du refactoring clairement dans le message de validation. Puis personne ne doit deviner pourquoi vous l'avez fait.

Tout refactoring qui touche de nombreux fichiers sera toujours plus difficile à examiner que celui qui ne touche que quelques-uns, ce qui ne fait aucune différence si vous le faites au début, au milieu ou à la fin du cycle de développement. Mais lorsque vous mélangez de gros refacteurs et nouvelles fonctionnalités d'un commettre, qui devient vraiment difficile à examiner, de sorte que cette méthode des règles 1.

Méthode 2 a l'inconvénient que vous n'aurez aucune chance de faire de TDD pour la nouvelle fonctionnalité X. Et après avoir été ajoutée X sans aucun test d'unité, il existe un certain risque d'oublier les tests par la suite, car l'effort supplémentaire de requérant Un refactoring important avant d'être en mesure d'ajouter des tests d'unité peut ne pas sembler la peine (ce qui est probablement une erreur, mais vous devrez peut-être expliquer cela à vos supérieurs).

De plus, je recommanderais

  • pour vous assurer d'avoir suffisamment de tests en place avant de commencer (pas nécessairement des tests unitaires) qui vous donnent confiance, le refactoring ne brise rien. Sinon, prenez auparavant le temps d'ajouter de tels tests.

  • pour vous assurer qu'une fois que vous avez terminé des tests d'unité X +, vous passez en revue le code par vous-même et vérifiez si le refactoring que vous avez fait au préalable atteint ses objectifs et si le code est vraiment dans un état propre maintenant. Sinon, ajoutez un refactoring supplémentaire/nettoyage étape par la suite.

10
Doc Brown

Le refactoring avant que la nouvelle fonctionnalité soit votre choix (méthode numéro 3), bien que je envisageais certainement de refactoriser plusieurs fois avant la nouvelle fonctionnalité. Si vous le pouvez, je recommanderais de casser votre refactoring dans des engagements individuels.

Bien que les grands engagements sont parfois inévitables (surtout avec le code plus âgé), plus le meilleur est le meilleur.

2
jmoreno

Comportements de refacteur en créant les sous-routines d'abord, quelques-uns ou un à la fois, modifiant les appelants, quelques-uns ou un à la fois, pour arrêter de réaliser ce comportement par code interne et appeler le sous-programme. Test de suites pour comparer les comportements avant et après peut être ajouté si le nom de l'appelant est modifié afin qu'ils existent tous deux à une révision pour des tests de comparaison, puis le code de code et le test de comparaison obsolescent peut être supprimé dans une autre version, en annulation du nouveau code à la version. Vieux nom afin que ses appelants ne doivent pas nécessairement être modifiés.

Une bonne couverture de test est bien sûr extrêmement importante: tous les cas étranges, exceptions, nulls, négatifs, diviser par zéro, nan, erreurs, bords et cas de centre de toutes sortes.

1

la méthode 3 est la plus courante, car

  1. vous développez un incrément de fonctionnalité et aucun incrément de fonctionnalité ne doit être possibilibile sans ses tests associés. Lorsque je tire une branche, la première chose que je fais est de regarder des tests pour trouver des exemples d'utilisation des API nouvellement développées. Ce commit est autoconstissant et documenté par le test

  2. Une fois que vous êtes en mesure de fournir un code de travail avec une nouvelle fonctionnalité à votre équipe, vous êtes prêt à refacturer le code. Le refactoring devrait être fait avec très peu de pas, sans casser aucun test. Les changements de rupture devraient arriver de la manière la plus sûre

Cette partie est ce que j'appelle "étendre, remplacer, supprimer"

supposons que vous ayez

public void executeBadCode(String what , String why) { ... }

et votre refactoring implique un changement de rupture telle que introduire un objet de paramètre.

donc, vous ajoutez une méthode

public void excuteBadCode(BadParams b) {
   executeBadCode(b.what() , b.why());
}

avec son test d'unité. Vous allez commettre spécifiquement que c'est un refactoring. par exemple. [refact] Introduction de la surcharge d'objets de paramètre pour la méthode ExecuteBadCode ()

Maintenant, je remplace tous les usages de la fonction précédente. Je le ferai un à l'heure, en faisant une seule commission par substitution dans une classe/fichier, ainsi que des modifications potentielles ou des ajouts dans des tests associés

Enfin, je retire la méthode ancienne déplaçant son corps dans le nouveau et en veillant à ce qu'il n'y ait aucune référence dans le code.

De cette manière, chaque développeur pourrait choisir une version de travail de votre code, peut l'examiner et peut comparer avec la précédente, détectant facilement le changement significatif.

0
Carmine Ingaldi