J'essaie généralement de suivre les conseils du livre Travailler efficacement avec Legacy Cod e . Je casse les dépendances, déplace des parties du code vers des @VisibleForTesting public static
méthodes et vers de nouvelles classes pour rendre le code (ou au moins une partie de celui-ci) testable. Et j'écris des tests pour m'assurer de ne rien casser lorsque je modifie ou ajoute de nouvelles fonctions.
Un collègue dit que je ne devrais pas faire ça. Son raisonnement:
- Le code d'origine pourrait ne pas fonctionner correctement en premier lieu. Et écrire des tests pour cela rend plus difficiles les corrections et modifications futures car les développeurs doivent aussi comprendre et modifier les tests.
- Si c'est du code GUI avec une certaine logique (~ 12 lignes, 2-3 si / else bloquent, par exemple), un test ne vaut pas la peine, car le code est trop trivial pour commencer.
- Des motifs similaires similaires pourraient également exister dans d'autres parties de la base de code (que je n'ai pas encore vu, je suis plutôt nouveau); il sera plus facile de tout nettoyer en un seul et même refactoring. Extraire la logique pourrait saper cette possibilité future.
Devrais-je éviter d'extraire des pièces testables et d'écrire des tests si nous n'avons pas le temps de refactoriser complètement? Y at-il un inconvénient à cela que je devrais considérer?
Réponses:
Voici mon impression personnelle non scientifique: les trois raisons semblent être des illusions cognitives répandues mais fausses.
la source
Quelques réflexions:
Lorsque vous refactorisez du code hérité, peu importe si certains des tests que vous écrivez contrediront les spécifications idéales. Ce qui compte, c'est qu'ils testent le comportement actuel du programme . Le refactoring consiste à prendre de petites étapes iso-fonctionnelles pour rendre le code plus propre; vous ne voulez pas vous engager dans la correction de bugs pendant votre refactoring. De plus, si vous repérez un bogue flagrant, il ne sera pas perdu. Vous pouvez toujours écrire un test de régression et le désactiver temporairement, ou insérer une tâche de correction de bug dans votre backlog pour plus tard. Une chose à la fois.
Je conviens que le code d'une interface graphique pure est difficile à tester et qu'il ne convient peut-être pas au refactoring de style " Travailler efficacement ... ". Toutefois, cela ne signifie pas que vous ne devez pas extraire un comportement qui n'a rien à voir avec la couche d'interface graphique et tester le code extrait. Et "12 lignes, 2-3 if / else block" n'est pas anodin. Tout code avec au moins un peu de logique conditionnelle doit être testé.
D'après mon expérience, les grandes refactorisations ne sont pas faciles et fonctionnent rarement. Si vous ne vous fixez pas d'objectifs précis et infimes, vous courez un risque élevé de retravailler sans cesse et de tirer les cheveux en poils, sans jamais tomber sur vos pieds. Plus le changement est important, plus vous risquez de casser quelque chose et plus vous aurez de difficulté à trouver où vous avez échoué.
Améliorer progressivement les choses avec de petites refactorisations ad hoc ne «mine pas les possibilités futures», mais leur permet de consolider le terrain marécageux où se situe votre application. Vous devriez certainement le faire.
la source
Voir aussi: "Le code d'origine pourrait ne pas fonctionner correctement" - cela ne signifie pas que vous modifiez simplement le comportement du code sans vous soucier de son impact. D'autres codes peuvent s'appuyer sur ce qui semble être un comportement défectueux ou sur les effets secondaires de la mise en œuvre actuelle. La couverture de test de l’application existante devrait faciliter la refactorisation ultérieure, car elle vous aidera à savoir quand vous avez accidentellement cassé quelque chose. Vous devriez d'abord tester les parties les plus importantes.
la source
La réponse de Kilian couvre les aspects les plus importants, mais je voudrais développer les points 1 et 3.
Si un développeur souhaite modifier le code (refactorisation, extension, débogage), il doit le comprendre. Elle doit s'assurer que ses changements affectent exactement le comportement qu'elle souhaite (rien dans le cas du refactoring), et rien d'autre.
S'il y a des tests, alors elle doit aussi les comprendre, bien sûr. Dans le même temps, les tests devraient l'aider à comprendre le code principal. De toute façon, les tests sont beaucoup plus faciles à comprendre que le code fonctionnel (à moins qu'il s'agisse de mauvais tests). Et les tests aident à montrer ce qui a changé dans le comportement de l'ancien code. Même si le code d'origine est incorrect et que le test teste ce comportement incorrect, c'est toujours un avantage.
Toutefois, cela nécessite que les tests soient documentés en tant que tests de comportement préexistant, et non de spécification.
Quelques réflexions également sur le point 3: outre le fait que le "grand coup" se produit rarement, il y a aussi une autre chose: ce n'est pas vraiment plus facile. Pour être plus facile, plusieurs conditions devraient s'appliquer:
XYZSingleton
? Est-ce que leur instance getter est toujours appeléegetInstance()
? Et comment trouvez-vous vos hiérarchies trop profondes? Comment recherchez-vous vos objets divins? Celles-ci nécessitent une analyse des métriques du code, puis une inspection manuelle des métriques. Ou vous tombez sur eux pendant que vous travaillez, comme vous le faisiez.la source
Dans certaines entreprises, certaines sociétés sont réticentes à permettre aux développeurs d’optimiser à tout moment un code qui n’apporte pas directement de valeur ajoutée, par exemple de nouvelles fonctionnalités.
Je prêche probablement aux convertis ici, mais c'est clairement une fausse économie. Un code propre et concis profite aux développeurs suivants. C'est simplement que le retour sur investissement n'est pas immédiatement évident.
Je souscris personnellement au principe du scoutisme, mais d'autres (comme vous l'avez vu) ne le font pas.
Cela dit, les logiciels souffrent d'entropie et créent une dette technique. Les développeurs précédents manquant de temps (ou peut-être simplement paresseux ou inexpérimenté) ont peut-être implémenté des solutions de buggy non optimales par rapport à des solutions bien conçues. Bien que cela puisse sembler souhaitable de les refactoriser, vous risquez d’introduire de nouveaux bogues dans le code qui fonctionne (pour les utilisateurs de toute façon).
Certains changements sont moins risqués que d'autres. Par exemple, là où je travaille, il y a souvent beaucoup de code dupliqué qui peut être transféré en toute sécurité à un sous-programme avec un impact minimal.
En fin de compte, vous devez vous prononcer sur le degré d'avancement de la refactorisation, mais l'ajout de tests automatisés, s'ils n'existent pas déjà, présente un intérêt indéniable.
la source
D'après mon expérience, un test de caractérisation fonctionne bien. Il vous donne une couverture de test étendue, mais pas très spécifique, assez rapidement, mais peut être difficile à implémenter pour les applications à interface graphique.
J'écrirais ensuite des tests unitaires pour les pièces que vous souhaitez modifier, et ce, chaque fois que vous souhaitez effectuer un changement, augmentant ainsi la couverture de vos tests unitaires au fil du temps.
Cette approche vous donne une bonne idée si les modifications affectent d’autres parties du système et vous permettent d’apporter les modifications nécessaires plus tôt.
la source
Re: "Le code d'origine pourrait ne pas fonctionner correctement":
Les tests ne sont pas gravés dans le marbre. Ils peuvent être changés. Et si vous avez testé une fonctionnalité incorrecte, il devrait être facile de réécrire le test plus correctement. Seul le résultat attendu de la fonction testée devrait avoir changé, après tout.
la source
Hé bien oui. Répondre en tant qu'ingénieur de test logiciel. Tout d’abord, vous devriez quand même tester tout ce que vous faites. Parce que si vous ne le faites pas, vous ne savez pas si cela fonctionne ou non. Cela peut sembler évident pour nous, mais j'ai des collègues qui voient les choses différemment. Même si votre projet est un petit projet qui ne sera peut-être jamais livré, vous devez regarder l'utilisateur en face et dire que vous savez que cela fonctionne parce que vous l'avez testé.
Le code non trivial contient toujours des bugs (citant un gars de l'université; et s'il n'y a pas de bugs, c'est trivial) et notre travail consiste à les rechercher avant le client. Le code hérité a des bogues hérités. Si le code d'origine ne fonctionne pas comme il se doit, vous voulez le savoir, croyez-moi. Les bugs sont acceptables si vous les connaissez, n’ayez pas peur de les trouver, c’est à cela que servent les notes de publication.
Si je me souviens bien, le livre sur le refactoring dit de tester constamment de toute façon. C'est donc une partie du processus.
la source
Faites la couverture de test automatisé.
Méfiez-vous des rêves illusoires, de vos propres clients, de ceux de vos clients et de vos supérieurs hiérarchiques. Même si j'aimerais beaucoup croire que mes modifications seront correctes la première fois et que je n'aurai à tester qu'une seule fois, j'ai appris à traiter ce type de réflexion de la même manière que je traite les courriels frauduleux nigérians. Eh bien, surtout; Je ne suis jamais allé pour un courriel frauduleux, mais récemment (quand crier dessus), j'ai cédé pour ne pas utiliser les meilleures pratiques. Ce fut une expérience douloureuse qui a traîné (cher) encore et encore. Plus jamais!
J'ai une citation préférée de la bande dessinée Web Freefall: "Avez-vous déjà travaillé dans un domaine complexe où le superviseur n'a qu'une idée approximative des détails techniques? ... Alors vous savez que le moyen le plus sûr de faire échouer son superviseur est de suivez chacun de ses ordres sans poser de questions. "
Il est probablement approprié de limiter le temps que vous investissez.
la source
Si vous avez affaire à de grandes quantités de code hérité qui ne sont pas actuellement testés, obtenir la couverture de test maintenant plutôt que d'attendre une grande réécriture hypothétique à l'avenir est la bonne solution. Commencer par écrire des tests unitaires n'est pas.
Sans test automatisé, après avoir apporté des modifications au code, vous devez effectuer des tests manuels complets de l'application pour vous assurer de son bon fonctionnement. Commencez par écrire des tests d'intégration de haut niveau pour remplacer cela. Si votre application lit les fichiers, les valide, les traite d'une manière ou une autre et affiche les résultats souhaités pour les tests.
Idéalement, vous disposez de données issues d'un plan de test manuel ou vous pouvez obtenir un échantillon des données de production réelles à utiliser. Si ce n'est pas le cas, puisque l'application est en production, dans la plupart des cas, elle fait ce qu'elle devrait être, alors créez simplement des données qui atteindront tous les points forts et supposerez que la sortie est correcte pour le moment. Ce n'est pas pire que de prendre une petite fonction, en supposant qu'elle porte le nom ou les commentaires suggérant qu'elle le devrait, et en écrivant des tests en supposant que cela fonctionne correctement.
Une fois que vous avez suffisamment écrit de ces tests de haut niveau pour capturer le fonctionnement normal des applications et les cas d'erreur les plus courants, vous aurez besoin de passer beaucoup de temps à cogner sur le clavier pour essayer de récupérer les erreurs du code en effectuant autre chose. vous pensiez que cela était supposé faire va considérablement réduire la future refactorisation (ou même une grosse réécriture) beaucoup plus facile.
Comme vous pouvez étendre la couverture des tests unitaires, vous pouvez réduire, voire annuler, la plupart des tests d'intégration. Si votre application lit / écrit des fichiers ou accède à une base de données, il est évident de commencer par tester ces parties séparément, puis de les imiter ou de faire en sorte que vos tests commencent par créer les structures de données lues à partir du fichier / de la base de données. En réalité, la création de cette infrastructure de test prendra beaucoup plus de temps que la rédaction d'un ensemble de tests rapides et sales; et chaque fois que vous exécutez un ensemble de tests d'intégration de 2 minutes au lieu de passer 30 minutes à tester manuellement une fraction de ce que les tests d'intégration couvraient, vous gagniez déjà beaucoup.
la source