Code hérité des tests unitaires : création d'applications maintenables

Si vous en avez assez de lire des articles sur la façon d'appliquer des tests unitaires à de nouvelles applications alors que la majeure partie de votre vie consiste à étendre et à améliorer le code hérité, voici un plan pour (enfin) exploiter les tests automatisés lorsque vous travaillez avec des applications existantes. C'est plus facile que vous ne le pensez, surtout si vous laissez Visual Studio et JustMock faire le gros du travail.
J'adore lire des articles sur la mise en route des tests unitaires automatisés, car ces articles sont presque totalement irréalistes. Tous ces articles supposent que vous créez une application entièrement nouvelle, ce qui, avouons-le, ne se produit pratiquement jamais. Nous savons tous que 70 à 90 % du temps d'un développeur est consacré à l'amélioration, l'extension, la modification et (parfois) la réparation d'applications déjà en production. Et je suis ici pour vous dire que personne n'est prêt à payer pour que vous encapsulez l'une de ces applications existantes/« héritées » dans des tests unitaires.
Mais, parce que vous passez la plupart de votre temps à apporter des modifications à ces applications héritées. , vous pouvez faire deux choses :
- Appliquer des tests unitaires aux zones que vous modifiez
- Utiliser un outil de moquerie (Telerik JustMockpar exemple) pour remplir les endroits que vous avez laissés seuls[19659006]Et cette stratégie est parfaitement logique car, après tout, la partie de l'application que vous n'avez pas touchée fonctionne toujours (probablement). Le point de danger, la partie qui vaut le test unitaire, est l'endroit où vous apportez des modifications. , j'utiliserai une application avec une page qui calcule les frais d'expédition d'un produit. Je suppose qu'une application ASP.NET MVC, mais ce que je vais faire fonctionnera aussi bien avec WebForms qu'une application de bureau.
Sur la page, l'utilisateur choisit le produit à expédier, sélectionne une quantité à expédier , et sélectionne une urgence (Élevée, Moyenne, Faible). L'utilisateur clique ensuite sur le bouton Soumettre de la page pour envoyer les données pour traitement.
Par conséquent, quelque part dans les entrailles de cette application ShippingManager, il existe une méthode qui traite les données de l'utilisateur : l'objet Produit (qui a un poids, une taille, largeur et instructions d'expédition spéciales comme « Fragile »), la quantité expédiée et l'urgence. Cette méthode appelle ensuite une méthode CalcShipCost, qui calcule les frais d'expédition à l'aide de toutes ces informations. Inutile de dire que la méthode CalcShipCost utilise également plusieurs variables globales déclarées au niveau de la classe (champs).
Cette méthode ressemble à ceci :
[HttpPost] public Résultat de l'action ShipProduct(IProduct prod, int qty, ShippingUrgency urg) { *[19659019*decimal shipCost = CalcShipCost(prod, qty, urg);; 19659019]** Modèle ShipAcceptModel = nouveau ShipAcceptModel(); modèle.ShipCost = shipCost; retour View(model); }[19659036]Voici le problème : la société a écrit CalcShippingCost lorsqu'elle a tout expédié dans un sens. L'entreprise souhaite maintenant continuer à utiliser cette méthode d'expédition, mais elle souhaite l'« étendre » afin que l'utilisateur puisse choisir parmi plusieurs méthodes d'expédition différentes (FedEx, UPS, USPS, etc.).
J'ai au moins trois emplois ( dont deux que j'ai créés pour moi-même) :
- Améliorez l'application pour prendre en charge les nouvelles méthodes d'expédition
- Faites-le d'une manière qui réduit les coûts de maintenance
- Faites-le d'une manière qui prend en charge les tests automatisés afin que je ne Je n'ai pas besoin de faire des tests de régression manuels pour prouver que cela fonctionne à chaque fois que j'apporte une modification
Refactoring pour la maintenabilité (et les tests)
une instruction switch dans CalcShipCosts qui teste chaque méthode d'expédition et fait ensuite la bonne chose. Si c'est le cas, alors vous avez raison : vous ne pourrez probablement pas faire de tests unitaires.
Bien sûr, vous aurez également une application qui entraîne des coûts importants chaque fois que vous devez ajouter ou modifier un mode d'expédition. . Avec chaque modification, vous devrez :
- Réécrire la méthode CalcShipCosts
- Effectuer des tests de régression manuels pour chaque scénario d'expédition pour vous assurer qu'ils fonctionnent toujours
Si vous implémentez le modèle de stratégie, vous obtiendrez un application à maintenir. Dans le modèle Stratégie, vous séparez les différents processus de calcul d'expédition afin qu'ils puissent être modifiés indépendamment les uns des autres et de l'application (ce modèle vous permettra également d'ajouter de nouveaux modes d'expédition sans avoir à réécrire les modes d'expédition existants ou l'application ).
Si vous profitez de quelques astuces de Visual Studio que je vais vous montrer en cours de route, le refactoring ne prendra pas longtemps (peut-être une heure) et vous pourrez le faire sans perturber le reste de l'application. Que vous obtiendrez également une application où vous pourrez éliminer les tests de régression manuels n'est que la cerise sur le gâteau.
La refactorisation pour le modèle Strategy signifie que ma méthode ShipProduct révisée ressemblera, au départ, à quelque chose comme ça (ça va devenir beaucoup plus simple) :
IShippingStrategy sStrat = new OriginalShip(); switch(meth) { Méthode d'expédition.USPS : sStrat = new USPSShip(); break; case ShippingMethod.FedEx : sStrat = nouveau FedexShip(); break; } décimal shipCost =[19659021]CalcShipCost(prod, qty, urg, sStrat); Modèle ShipAcceptModel = nouveau ShipAcceptModel(); modèle.ShipCost = shipCost; retour View(model); }[19659036] Et j'ai juste tapé le code tel que vous le voyez, en ignorant toutes les lignes ondulées rouges que Visual Studio génère car il n'a jamais entendu parler de ces nouvelles classes. Chacune de ces nouvelles classes (FedExShip, UPSShip, etc.) contiendra le code propre à une méthode d'expédition (je les appellerai « classes stratégiques »). Pour m'assurer que toutes mes classes de stratégie seront interchangeables, j'invente également une interface IShipping pour toutes mes classes de stratégie à implémenter.[HttpPost] public ActionResult ShipProduct(IProduct prod, int qty, ShippingUrgency urg, ShippingMethod meth)[1945901819659017]{
Ma prochaine étape consiste à faire en sorte que Visual Studio génère mes interfaces et classes pour moi. Je passe ma souris sur la référence à IShippingStrategy, clique sur l'onglet intelligent qui apparaît devant le nom de la classe et sélectionne "Générer l'interface 'IShippingStrategy' dans un nouveau fichier" dans le menu résultant. Je répète cela pour toutes mes classes de stratégie et, tout à coup, j'ai plusieurs nouveaux fichiers. Cela fait apparaître une boîte de dialogue dans laquelle je précise que je veux que ce nouveau type soit une énumération publique créée dans un nouveau fichier. Je clique sur OK et, encore une fois, j'obtiens un nouveau fichier.
Malheureusement, Visual Studio a placé ces nouveaux fichiers dans le dossier Controllers. Dans l'Explorateur de solutions, je fais glisser ces nouveaux fichiers là où je le souhaite dans le dossier Modèles. Pendant que je fais cela, je change la portée de chaque classe d'interne à publique et mets à jour leurs espaces de noms pour leur nouvel emplacement. Je remplis également mon enum avec ses valeurs (USPS, FedEx, etc.).
Enfin, ma méthode CalcShipCost n'est pas configurée pour accepter mon nouveau paramètre IShippingStrategy. Pour résoudre ce problème, je passe la souris sur mon appel à CalcShipCost, je clique sur la balise active et je sélectionne « Ajouter un paramètre à… ». Visual Studio corrige ma méthode CalcShipCost pour accepter le nouveau paramètre.
Génération de mon premier objet de stratégie
Je ne jette pas cette méthode CalcShipCost pour deux raisons. Premièrement, la méthode contient un code indépendant de la méthode d'expédition. Deuxièmement, la société va continuer à utiliser le code dans la méthode qui dépend de la méthode d'expédition actuelle. Cela signifie que je vais supprimer la partie du CalcShipCost existant qui dépend de la méthode d'expédition actuelle et la déplacer vers ma nouvelle classe OriginalShip (je la mettrai dans une méthode que j'appellerai CalcMethodCost).
Plus précisément. : Je vais demander à Visual Studio de faire tout cela. block” solution):
private decimal CalcShipCost(IProduct prod, int qty, ShippingUrgency urg , IShippingStrategy sStrat) { decimal Suppléments = 0; bool sont des Suppléments = ; décimal frais d'expédition = 0; if (prod.poids > 100) { frais d'expédition += 100 * Taxe de vente; } if (qty < 50[19659015]) { ….plus de code …} commutateur (urg) { …diverses déclarations de cas } si (sont en supplément) { shippingCost += extraCharges; } return shippingCost; }
Au fait, si vous avez regardé ce code assez étroitement pour se demander d'où vient SalesTax, c'est l'un de ces champs que j'ai mentionnés plus tôt, déclaré au niveau de la classe et utilisé partout dans l'application.
Créer mon premier objet de stratégie est facile si j'utilise Visual Studio et le fais en deux étapes. Je sélectionne d'abord le code dans ma méthode CalcShipCost d'origine que je veux déplacer dans ma nouvelle méthode, fais un clic droit sur la sélection et choisis "Actions rapides et refactorisations" dans le menu contextuel. Cela affiche un autre menu où je sélectionne "Extract Method".
À ce stade, Visual Studio affiche une boîte de dialogue, extrait le code que j'ai sélectionné et le remplace par un appel à une nouvelle méthode nommée, intelligemment, NewMethod. Je renomme NewMethod en CalcMethodCost dans mon code d'origine et clique sur le bouton Appliquer dans la boîte de dialogue. Visual Studio crée comme par magie ma nouvelle méthode.
Je clique sur mon nouvel appel de méthode et appuyez sur F12 pour accéder à la méthode. Une fois là-bas, je coupe la méthode de l'application ShippingManager et la colle dans ma classe OriginalShip : j'ai ma première classe de stratégie. Je supprime le modificateur statique ajouté à la méthode et change la portée de la méthode de private à public.
Enfin, de retour dans CalcShipCost, j'appelle ma nouvelle méthode à partir du paramètre IShippingStrategy :
private decimal CalcShipCost (IProduit de produit, int qté, ShippingUrgency urg, IShippingStrategy sStrat) { { decimal extraCharges = 0; bool areExtraCharges = false; **[19459004[]shippingCost = sStrat.CalcMethodCost(prod, qty, urg);[196590** si (sont en supplément) { shippingCost += extraCharges; } return shippingCost; }
Et voici mon premier objet de stratégie :
{ } }public class OriginalShip: IShippingStrategy { public decimal CalcMethodCost(IProduct prod, int qty, ShippingUrgency][194590
Je peux maintenant créer mon interface : je copie la première ligne de ma méthode CalcMethodCost, la colle dans IshippingStrategy, supprime la portée publique et mets un point-virgule à la fin :[19659187]public interface IShippingStrategy
{
decimal CalcMethodCost(IProduct prod, int qty, ShippingUrgency urg);
}
Je demande ensuite à Visual Studio de faire une génération et de découvrir que ma nouvelle classe OriginalShip ne sait pas d'où vient SalesTax (ou, d'ailleurs, tout autre champ de ShippingManager). La solution la plus simple consiste simplement à ajouter SalesTax en tant qu'autre valeur transmise à l'appel à CalcMethodCost et à utiliser à nouveau « Ajouter un paramètre à … ». Malheureusement, cela ne fait que mettre à jour mon interface, je dois donc ajouter moi-même le paramètre à CalcMethodCost dans OriginalShip.
Avec mon interface IShippingStrategy maintenant définie, Visual Studio implémentera cette interface dans mes autres classes de stratégie : nom de l'interface dans chaque classe de stratégie, cliquez sur l'onglet intelligent qui apparaît et sélectionnez "Implémenter l'interface". Mon code se compile maintenant, tous mes objets de stratégie ont une méthode CalcMethodCost, et j'ai pu terminer mon refactoring. Je vais apporter une autre modification à la méthode ShipProduct : avoir le code qui crée l'objet de stratégie dans ma méthode d'origine limite ma capacité à ajouter de nouvelles méthodes d'expédition. Je décide d'implémenter le modèle de méthode d'usine en plaçant le bloc de commutation qui sélectionne le bon objet de stratégie d'expédition dans une méthode dans une classe à part. code à ShippingManager pour instancier ma classe d'usine, puis que Visual Studio crée la classe. Je sélectionne le bloc de commutation et utilise la méthode Extract pour mettre le bloc dans une méthode. Après avoir appuyé sur F12 pour passer à la méthode, je la coupe de ShippingManager et la colle dans ma nouvelle classe. Une fois la méthode déplacée, je supprime son modificateur statique et change la portée privée de la méthode en public. Je rends également la classe publique, modifie son espace de noms et fais glisser la classe dans le dossier Models. Enfin, le code d'origine dans ShippingManager utilise la nouvelle classe lorsqu'il appelle la méthode.
Cela signifie que ma méthode d'origine ressemble maintenant à ceci :
[HttpPost] public ActionResult ShipProduct(IProduct prod, int qty, ShippingUrgency urg, ShippingMethod meth)[1945901819659017]{ ShippingMethodFactory smf = new ShippingMethodFactory(); IShippingStrategy sStrat = smf.GetShippingMethod(meth); decimal shipCost] (prod, qté, urg, meth, sStrat);[19659220]return View(); }
Ma classe ShippingMethodFactory est assez simple et ressemble à ceci :
public class ShippingMethodFactory[19659017]{ public IShippingStrategy GetShippingMethod(ShippingMethod meth) { IShippingStrategy sStrat = new OriginalShip(); switch (meth) }{[1965959] return sStrat; } }
Cette factorisation supplémentaire prend environ cinq minutes de plus. Désormais, si l'entreprise ajoute une autre méthode d'expédition, je peux réécrire ma méthode d'usine pour renvoyer l'objet de stratégie associé et laisser ma méthode CalcShipCost seule.
Il y a d'autres avantages : s'il s'avère qu'un objet de stratégie nécessite du code de configuration pour fonctionne correctement, je peux également le mettre dans ma méthode d'usine. Si je fais cela, alors, lorsqu'un développeur a besoin d'un objet de stratégie d'expédition, il peut simplement appeler GetShippingMethod et être sûr qu'il obtient un objet prêt à être utilisé.
Le refactoring final
À ce stade, je réalisez que, si je fais une refactorisation supplémentaire et déplace l'appel à CalcShipCost dans sa propre classe, j'aurai un calculateur d'expédition autonome. Cela me positionnera également pour effectuer des tests unitaires au niveau de cette nouvelle calculatrice, un test au niveau des composants. Cela me semble intéressant.
Encore une fois, j'utilise Visual Studio pour réécrire ma méthode ShipProduct d'origine à partir de ceci :
ShippingMethodFactory smf = new ShippingMethodFactory() ; IShippingStrategy sStrat = smf.GetShippingMethod(meth); decimal shipCost] (prod, qté, urg, meth, sStrat);[19659036]À ceci :
ShippingCostCalculator scc = nouveau ShippingCostCalculator(); décimal shipCost= .CalcShipping(prod, qté, urg, meth, SalesTax );
Ma nouvelle classe ressemble à ceci :
public class ShippingCostCalculator { public decimal CalcShipping( IProd produit, int qté, ShippingUrgency urg, ShippingMethod meth, decimal salesTax) { ShippingMethodFactory smf = new ShippingMethodFactory(); IShippingStrategy sStrat = smf.GetShippingMethod(meth); return Calc196590Cost , qté, urg, sStrat, taxe de vente); } privé[19659016]decimal CalcShipCost(IProduct prod, int qty, ShippingUrgency urg, decimalT ventes ]) { }
Cela me permet d'écrire des tests au niveau des composants pour ma nouvelle calculatrice qui prouveront que tout mon code fonctionne ensemble pour fournir la réponse attendue par l'entreprise.
Regard en arrière
Vous pourriez faire moins que ce que j'ai fait ici. Vous pourriez décider que vous n'avez pas besoin de la classe d'usine. Vous pouvez décider que si vos tests au niveau unitaire fonctionnent, vous pouvez passer directement à l'intégration et ne pas vous soucier de vous positionner pour les tests au niveau des composants. (Je soulignerai simplement que si une autre application devait calculer les frais d'expédition du produit, au prix de la mise à jour de certains espaces de noms, je pourrais déplacer ShippingCostCalculator vers sa propre bibliothèque de classes et la rendre généralement disponible.)
Mais, peu importe de combien vous faites, vous avez un meilleur design. Nous sommes passés d'une application d'expédition monolithique à une application d'expédition avec un ensemble de classes de principe de responsabilité unique : une classe de coûts (ShippingCostCalculator), une classe d'usine (ShippingMethodFactory) et une classe de stratégie d'expédition pour chaque méthode d'expédition prise en charge par l'entreprise (FedExShip, UPSShip, etc.).
Donc, après environ une heure à une heure et demie de refactorisation, j'ai une conception hautement maintenable et - un heureux accident - une conception que je peux tester unitairement. Ce qui est une bonne chose car, après tout ce piratage et ce slash, je ne sais plus si le code fonctionne réellement. C'est mon prochain post.
Source link