J'ai lu des réponses à des questions allant dans le même sens, telles que "Comment maintenez-vous vos tests unitaires pendant le refactoring?". Dans mon cas, le scénario est légèrement différent dans la mesure où on m'a confié un projet à réviser et à mettre en conformité avec certaines normes en vigueur. À l'heure actuelle, il n'y a aucun test pour le projet!
J'ai identifié un certain nombre de choses qui, à mon avis, auraient pu être améliorées, telles que le non-mélange du code de type DAO dans une couche de service.
Avant de refactoriser, il semblait être une bonne idée d'écrire des tests pour le code existant. Le problème qui m’apparaît est que lorsque je refacturais, alors ces tests se briseront au fur et à mesure que je changerai de logique et que les tests seront écrits avec la structure précédente à l’esprit (dépendances simulées, etc.).
Dans mon cas, quelle serait la meilleure façon de procéder? Je suis tenté de rédiger les tests autour du code refactorisé, mais je suis conscient du risque de refactoriser de manière incorrecte des éléments susceptibles de modifier le comportement souhaité.
Qu'il s'agisse d'un refactor ou d'une refonte, je suis heureux que ma compréhension de ces termes soit corrigé. Je travaille actuellement sur la définition suivante pour le refactoring "Avec le refactoring, par définition, vous ne changez pas ce que fait votre logiciel, vous changez la façon dont il le fait ". Donc, je ne change pas ce que le logiciel fait, je changerais comment / où il le fait.
De même, je peux voir que si je modifie la signature des méthodes, cela pourrait être considéré comme une refonte.
Voici un bref exemple
MyDocumentService.java
(courant)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
DataResultSet rs = documentDAO.findAllDocuments();
List<Document> documents = new ArrayList<>();
for(DataObject do: rs.getRows()) {
//get row data create new document add it to
//documents list
}
return documents;
}
}
MyDocumentService.java
(refactored / redessiné quoi que soit)
public class MyDocumentService {
...
public List<Document> findAllDocuments() {
//Code dealing with DataResultSet moved back up to DAO
//DAO now returns a List<Document> instead of a DataResultSet
return documentDAO.findAllDocuments();
}
}
la source
Réponses:
Vous recherchez des tests qui vérifient les régressions . c'est-à-dire rompre un comportement existant. Je commencerais par identifier à quel niveau ce comportement restera le même, l'interface que la conduite de ce comportement restera le même et commencer à mettre en place des tests à ce stade.
Vous avez maintenant des tests qui affirmeront que quoi que vous fassiez en dessous de ce niveau, votre comportement reste le même.
Vous avez parfaitement raison de vous demander comment les tests et le code peuvent rester synchronisés. Si votre interface avec un composant reste la même, vous pouvez alors écrire un test autour de celui-ci et affirmer les mêmes conditions pour les deux implémentations (lors de la création de la nouvelle implémentation). Si ce n'est pas le cas, vous devez accepter le fait qu'un test pour un composant redondant est un test redondant.
la source
La pratique recommandée consiste à commencer par écrire des "tests" qui testent le comportement actuel du code, y compris éventuellement des bogues, mais sans vous obliger à plonger dans la folie de discerner si un comportement donné qui enfreint les exigences est un bogue, solution de contournement pour quelque chose que vous ignorez ou représente un changement non documenté des exigences.
Il est plus logique que ces tests de précision se situent à un niveau élevé, c’est-à-dire des tests d’intégration plutôt que des tests unitaires, afin qu’ils continuent de fonctionner lorsque vous démarrez le refactoring.
Toutefois, certaines refactorisations peuvent être nécessaires pour rendre le code testable - veillez à vous en tenir à des refactorisations "sûres". Par exemple, dans presque tous les cas, les méthodes privées peuvent être rendues publiques sans rien casser.
la source
Si vous ne l'avez pas déjà fait, je suggère de lire à la fois Travailler efficacement avec le code hérité et Refactoriser - Améliorer la conception du code existant .
Je ne vois pas nécessairement cela comme un problème: écrivez les tests, modifiez la structure de votre code, puis ajustez également la structure du test . Cela vous indiquera directement si votre nouvelle structure est réellement meilleure que l’ancienne, car si c’est le cas, les tests ajustés seront plus faciles à écrire (et donc changer les tests devrait être relativement simple, ce qui réduirait le risque d’introduction récente. bug passer les tests).
En outre, comme d’autres l’ont déjà écrit, n’écrivez pas des tests trop détaillés (du moins pas au début). Essayez de rester à un niveau d'abstraction élevé (vos tests seront donc probablement mieux caractérisés par des tests de régression ou même d'intégration).
la source
N'écrivez pas de tests unitaires stricts où vous vous moquez de toutes les dépendances. Certaines personnes vous diront que ce ne sont pas de vrais tests unitaires. Ignore les. Ces tests sont utiles et c'est ce qui compte.
Regardons votre exemple:
Votre test ressemble probablement à ceci:
Au lieu de vous moquer de DocumentDao, moquez-vous de ses dépendances:
Maintenant, vous pouvez déplacer la logique de
MyDocumentService
dansDocumentDao
sans que les tests ne se brisent. Les tests montreront que la fonctionnalité est la même (dans la mesure où vous l'avez testée).la source
Comme vous le dites, si vous modifiez le comportement, il s’agit d’une transformation et non d’un refactor. À quel niveau vous modifiez le comportement est ce qui fait la différence.
S'il n'y a pas de test formel au plus haut niveau, essayez de trouver un ensemble d'exigences auxquelles les clients (code d'appel ou humains) doivent rester inchangés après votre nouvelle conception pour que votre code soit considéré comme opérationnel. C'est la liste des cas de test que vous devez implémenter.
Pour répondre à votre question sur la modification des implémentations nécessitant des scénarios de test différents, je vous suggère de jeter un coup d'œil à Detroit (classique) vs London (mockist) TDD. Martin Fowler en parle dans son excellent article Les moqueries ne sont pas des moignons, mais beaucoup de gens ont des opinions. Si vous commencez au plus haut niveau, où vos externals ne peuvent pas changer, et que vous réduisez votre temps, les exigences doivent rester relativement stables jusqu'à ce que vous atteigniez un niveau qui doit réellement changer.
Sans aucun test, la tâche sera difficile et vous voudrez peut-être envisager d'exécuter les clients selon deux chemins de code (et enregistrer les différences) jusqu'à ce que vous puissiez être sûr que votre nouveau code fasse exactement ce qu'il doit faire.
la source
Voici mon approche. Cela a un coût en termes de temps car il s'agit d'un test de refactorisation en 4 phases.
Ce que je vais exposer convient peut-être mieux aux composants plus complexes que celui exposé dans l'exemple de la question.
Quoi qu'il en soit, la stratégie est valable pour tout composant candidat à normaliser par une interface (DAO, Services, Contrôleurs, ...).
1. l'interface
Permet de rassembler toutes les méthodes publiques de MyDocumentService et de les rassembler dans une interface. Par exemple. S'il existe déjà, utilisez celui-là au lieu d'en créer un nouveau .
Ensuite, nous forçons MyDocumentService à implémenter cette nouvelle interface.
Jusqu'ici tout va bien. Aucun changement majeur n'a été apporté, nous avons respecté le contrat actuel et behaivos reste intact.
2. Test unitaire du code hérité
Ici nous avons le travail difficile. Pour configurer une suite de tests. Nous devrions définir autant de cas que possible: des cas réussis mais aussi des cas d'erreur. Ces dernières sont pour le bien de la qualité du résultat.
Au lieu de tester MyDocumentService, nous allons utiliser l'interface comme contrat à tester.
Je ne vais pas entrer dans les détails, alors pardonnez-moi si mon code est trop simple ou trop agnostique
Cette étape prend plus de temps que toute autre dans cette approche. Et c'est le plus important, car cela servira de point de référence pour les comparaisons futures.
Remarque: En raison de l'absence de modifications majeures, behaivor reste inchangé. Je suggère de faire une étiquette ici dans le SCM. Le tag ou la branche n'a pas d'importance. Il suffit de faire une version.
Nous le voulons pour les annulations, les comparaisons de versions et peut être pour les exécutions en parallèle de l'ancien code et du nouveau.
3. Refactoring
Refactor va être implémenté dans un nouveau composant. Nous ne ferons aucune modification sur le code existant. La première étape est aussi simple que de copier-coller de MyDocumentService et de le renommer en CustomDocumentService (par exemple).
La nouvelle classe continue d'implémenter DocumentService . Puis allez et refactorisez getAllDocuments () . (Commençons par un. Pin-refactors)
Cela peut nécessiter quelques modifications sur l'interface / les méthodes de DAO. Si c'est le cas, ne changez pas le code existant. Implémentez votre propre méthode dans l'interface DAO. Annotez l'ancien code avec obsolète et vous saurez plus tard ce qui doit être supprimé.
Il est important de ne pas casser / changer la mise en œuvre existante. Nous voulons exécuter les deux services en parallèle, puis comparer les résultats.
4. Mise à jour de DocumentServiceTestSuite
Ok, maintenant la partie la plus facile. Ajouter les tests du nouveau composant.
Nous avons maintenant oldResult et newResult validés indépendamment, mais nous pouvons également comparer les uns avec les autres. Cette dernière validation est facultative et dépend du résultat. Peut-être que ce n'est pas comparable.
Ne comparez peut-être pas deux collections de cette façon, mais serait valable pour tout autre type d’objet (pojos, entités de modèle de données, DTO, wrappers, types natifs, etc.).
Remarques
Je n'oserais pas dire comment faire des tests unitaires ou comment utiliser des librairies factices. Je n'ose pas non plus dire comment vous devez faire le refactor. Ce que je voulais faire, c'est suggérer une stratégie globale. Comment aller de l'avant dépend de vous. Vous savez exactement ce qu'est le code, sa complexité et si une telle stratégie vaut la peine d'être essayée. Des faits tels que le temps et les ressources sont importants ici. En outre, qu'attendez-vous de ces tests à l'avenir?
J'ai commencé mes exemples par un service et je suivrais avec DAO et ainsi de suite. Aller au fond des niveaux de dépendance. Plus ou moins, cela pourrait être décrit comme une stratégie ascendante . Cependant, pour les modifications / refactorisations mineures ( comme celle exposée dans l'exemple de tournée ), un processus ascendant faciliterait la tâche. Parce que la portée des changements est faible.
Enfin, il vous appartient de supprimer le code obsolète et de rediriger les anciennes dépendances vers la nouvelle.
Supprimez également les tests obsolètes et le travail est terminé. Si vous avez mis à niveau l'ancienne solution avec ses tests, vous pouvez vérifier et comparer à tout moment.
En conséquence de tant de travail, vous avez testé, validé et mis à jour le code existant. Et un nouveau code, testé, validé et prêt à être mis en version.
la source
tl; dr N'écrivez pas de tests unitaires. Rédigez des tests à un niveau plus approprié.
Compte tenu de votre définition de travail du refactoring:
le spectre est très large. D'un côté, il y a un changement autonome à une méthode particulière, qui utilise peut-être un algorithme plus efficace. À l'autre extrémité est le portage dans une autre langue.
Quel que soit le niveau de refactorisation / refonte utilisé, il est important que les tests fonctionnent à ce niveau ou à un niveau supérieur.
Les tests automatisés sont souvent classés par niveau comme suit:
Tests unitaires - Composants individuels (classes, méthodes)
Tests d'intégration - Interactions entre composants
Tests du système - L'application complète
Ecrire le niveau de test qui peut supporter la refactorisation essentiellement intacte.
Pense:
la source
Ne perdez pas de temps à écrire des tests qui se rattachent à des points où vous pouvez anticiper que l’interface va changer de manière non triviale. C'est souvent le signe que vous essayez de tester des classes qui sont de nature «collaborative» - leur valeur ne réside pas dans ce qu'elles font elles-mêmes, mais dans la façon dont elles interagissent avec un certain nombre de classes étroitement liées pour produire un comportement précieux . C'est ce comportement que vous souhaitez tester, ce qui signifie que vous souhaitez effectuer des tests à un niveau supérieur. Tester à un niveau inférieur à ce niveau nécessite souvent beaucoup de moqueries laides, et les tests qui en résultent peuvent être plus un frein au développement qu'une aide à la défense du comportement.
Ne vous attardez pas trop sur le fait que vous fassiez un refactor, une refonte, etc. Vous pouvez apporter des modifications qui, au niveau inférieur, constituent une refonte d'un certain nombre de composants, mais à un niveau d'intégration supérieur, il s'agit simplement d'un refactor. Le but est de définir clairement le comportement qui vous est utile et de le défendre au fur et à mesure.
Il serait peut-être utile de prendre en compte vos tests lors de la rédaction de vos tests. Pourrais-je facilement décrire à un responsable de l'assurance qualité, un propriétaire de produit ou un utilisateur, ce que ce test teste en réalité? S'il semble que la description du test soit trop ésotérique et technique, vous effectuez peut-être des tests au mauvais niveau. Testez à des points / niveaux qui «ont du sens», et ne gommez pas votre code avec des tests à tous les niveaux.
la source
Votre première tâche consiste à essayer de trouver la "signature de méthode idéale" pour vos tests. Efforcez-vous d'en faire une fonction pure . Cela devrait être indépendant du code qui est actuellement à l’essai; c'est une petite couche d'adaptateur. Écrivez votre code sur cette couche d’adaptateur. Désormais, lorsque vous refactorisez votre code, il vous suffit de changer le calque de l'adaptateur. Voici un exemple simple:
Les tests sont bons, mais le code sous test a une mauvaise API. Je peux le refactoriser sans changer les tests en mettant simplement à jour ma couche d'adaptateur:
Cet exemple semble être une chose assez évidente à faire selon le principe «Ne vous répétez pas», mais cela peut ne pas être aussi évident dans d'autres cas. L'avantage va au-delà de DRY - le véritable avantage est le découplage des tests du code sous test.
Bien sûr, cette technique peut ne pas être recommandée dans toutes les situations. Par exemple, il n'y aurait aucune raison d'écrire des adaptateurs pour les POCO / POJO car ils ne disposent pas vraiment d'une API susceptible de changer indépendamment du code de test. De même, si vous écrivez un petit nombre de tests, une couche d’adaptateur relativement grande serait probablement un effort inutile.
la source