Fermer

septembre 29, 2021

Tests unitaires Legacy Code, Partie 2 : Exploitation d'objets fictifs


À quoi ressemblent les tests unitaires après avoir refactorisé une application héritée pour la rendre plus facile et moins chère à maintenir ?

La plupart des développeurs passent la plupart de leur temps à étendre, améliorer, modifier et (parfois) réparer les applications « anciennes » : ce qu'on appelle la maintenance logicielle. Appliquer des tests unitaires dans cet environnement peut sembler difficile, voire impossible. Ce n'est pas le cas.

Dans un article précédentj'ai examiné une application « héritée » typique et une activité de maintenance typique : étendre une méthode de coût d'expédition pour gérer plusieurs méthodes d'expédition, y compris la méthode d'expédition actuelle. Dans cet article, j'ai expliqué comment cette méthode pourrait être remaniée pour créer une meilleure conception qui réduirait à la fois les coûts de maintenance et créerait un composant réutilisable de « calculateur de coûts d'expédition » comme effet secondaire. Parce que j'ai profité des outils de Visual Studio, cette refactorisation aurait pris environ une heure (une heure et demie à l'extérieur).

Et, oui, cette conception me permettrait également de mettre en œuvre des tests unitaires. C'est donc le sujet de cet article : à quoi ressemblent les tests unitaires après avoir refactorisé une application héritée pour la rendre plus facile et moins chère à maintenir ?

Dans ce cas, cela signifie tester les objets résultant de ma refactorisation :[19659006]Un calculateur d'expédition, qui est appelé depuis l'application d'origine et contrôle le processus

  • Un ensemble d'objets de coût pour chaque méthode d'expédition (par exemple, FedExShip, USPSShip, etc.)
  • Une classe « usine » qui sélectionne et configure le bon objet de coût d'expédition
  • Comme je l'ai dit dans ce post précédent, vous auriez peut-être opté pour une conception plus simple. Quel que soit le niveau de refactorisation que vous avez effectué, vous êtes maintenant prêt à commencer les tests.

    Première étape : conception de tests

    Parce que l'entreprise souhaite conserver la méthode d'expédition d'origine, avant d'écrire des tests, je dois tester l'application d'origine pour déterminer ce qui compte actuellement comme une bonne réponse. La spécification logicielle d'origine et tous les plans de test existants m'aideront à choisir mes cas de test, mais je devrai probablement essayer l'application moi-même. Cela pourrait être le dernier test de régression manuel que cette partie de l'application verra jamais.

    Lorsque j'aurai terminé cette étape, j'aurai un ensemble de résultats de test que je pourrai utiliser pour prouver que ma nouvelle version fait toujours ce que la version existante le fait (et j'ai peut-être aussi commencé à générer des cas de test pour les autres méthodes d'expédition).

    Je dois ensuite améliorer mes données de test. Les résultats que j'ai obtenus lors de mon premier passage comprenaient à la fois le coût lié à la méthode d'expédition (code qui est maintenant dans une classe appelée OriginalShip) et tous les autres frais d'expédition (le code laissé dans la méthode d'origine, appelé CalcShipCost). Pour tous mes cas de test de méthode d'origine, je dois déterminer quelles données le code de mon objet OriginalShip doit renvoyer. pour fonctionner, je devrai tester "l'ensemble du processus": Personne ne se souciera de savoir si OriginalShip fait bien sa part si le résultat du calculateur d'expédition dans son ensemble est erroné.

    La génération de ce deuxième niveau de données de test m'en donne un autre possibilité de vérifier si mon refactoring a été fait correctement. Il ne serait pas surprenant, après tout, si je découvrais que, parce que je ne comprenais pas parfaitement quelles pièces étaient liées à la méthode d'expédition et quelles pièces ne l'étaient pas, mon premier objet de coût n'est pas tout à fait correct. Et, encore une fois, cela me permettra probablement de commencer à générer les données de test pour mes autres objets de coût (USPSShip, FedExShip, etc.).

    Cela positionne également mon application pour prendre en charge deux niveaux de test. Au premier niveau (le niveau de test unitaire), je peux tester mes objets de coût pour m'assurer qu'ils fonctionnent correctement et me préparer à tester tout nouvel objet de coût sur toute la ligne. Lorsque je sais que n'importe quel objet d'évaluation fonctionne, je peux passer à mon deuxième niveau de test : des tests au niveau des composants qui prouvent que mes objets d'évaluation fonctionnent avec le calculateur d'expédition.

    Il y a un réel avantage à avoir un test au niveau des composants : La calculatrice renvoie le coût d'expédition complet, le nombre que mes utilisateurs voient et ce qui est stocké dans la base de données – la "réponse commerciale". C'est beaucoup plus facile à vérifier et à faire signer par mes utilisateurs que le résultat intermédiaire de mes objets de coût.

    Deuxième étape : écriture des tests de niveau unitaire

    Je commence à automatiser mes tests de régression en ajoutant un nouveau projet à ma solution, en utilisant le modèle de projet de test C# JustMock qui est installé dans Visual Studio lorsque vous installez JustMock. Ce projet comprend une classe de test par défaut nommée JustMockTest que je vais renommer en quelque chose de plus spécifique.

    Une correction est requise à ce stade : je dois ajouter à mon projet de test une référence à mon application. A terme, j'aurai besoin de mock objects donc j'active aussi le profileur JustMock (Extensions | JustMock | Enable Profiler).

    Test de l'usine

    Avec mes premiers tests, je teste l'objet usine qui sélectionne et configure mes expédition objets de coût : je veux prouver qu'il me donne mon objet OriginalShip lorsque je le demande. Dans ma classe de test nouvellement renommée, je renomme la méthode de test par défaut de TestMethod en GetOriginalShippingMethodTest. Comme j'ai déjà généré mes objets de coût, mon premier test ressemble à ceci :

    [TestMethod]
    public void GetOriginalShippingMethodTest()[19659030]{
       ShippingMethodFactory smf = new ShippingMethodFactory();
    
       IShippingCosting sStrat = smf.GetShippingMethod(ShippingMethod.USPS);.USPS);[1945
    
       Assert.N'est pas Null(sStrat);
       Assert.IsInstanceOfType(sStrat, typeof(IShippingCosting));
       Assert.IsInstanceOfType(sStrat, typeof(USPSShip));
    } 19659055] Si ma méthode d'expédition nécessitait une configuration spéciale, j'exposerais ces options via des propriétés en lecture seule dans l'interface IShippingCosting et les vérifierais également dans ce test.

    Et (étonnamment pour moi) mon premier test réussit - le l'avantage de tirer parti de Visual Studio et de couper/coller le code existant lorsque j'ai effectué ma refactorisation d'origine. Selon mon niveau de paranoïa, je pourrais créer des tests supplémentaires pour chacun de mes objets de coût (si j'utilisais XUnit au lieu de MSTest, l'attribut ClassData rendrait cela facile).

    Test de l'objet de coût

    Maintenant, je' Je suis prêt à tester ma classe OriginalShipping. Je préférerais que mon nouveau test soit aussi isolé que possible de toutes les autres classes - de cette façon, je sais que si quelque chose ne va pas, c'est la faute de l'objet OriginalShip.

    Pour rendre le problème plus intéressant, le produit qui est passé à mon La méthode ShipProduct (et que je transmets à mon CalcMethodCost) n'est qu'une interface - je ne sais pas vraiment quelle classe est utilisée. Je pourrais utiliser la classe ProductRepo qui renvoie les objets Product de la base de données pour me donner un exemple d'objet IProduct… mais cela ne me donnerait pas l'isolement que je souhaite : toute modification apportée à la base de données pourrait faire dérailler mon test.

    Ce n'est pas le cas. un scénario inhabituel et c'est exactement la raison pour laquelle des outils de moquerie (comme JustMock) existent. Je peux utiliser JustMock pour m'assurer que j'obtiens toujours les objets IProduct que je veux pour mon test, même lorsque tout ce que j'ai est une interface.

    Le début de mon test crée un objet fictif pour l'interface IProduct qui spécifie les valeurs pour divers propriétés que je veux pour mon test. Pour isoler davantage mon test, je crée également un objet fictif pour retourner l'usine d'expédition que je veux pour le test :

    [TestMethod]
    public void OriginalShipProduct1Test ()
    {
       IProduct prodMock = Mock.Créer<IProduct>();
       Mock.Arrange(() => prodMock.ProdId).[19659036]Retours("A123");
       Mock.Disposer(() => prodMock.poids).[19659036]Retours(200);
       
    
       ShippingMethodFactory sf = new ShippingMethodFactory();
       Mock.Organiser(() => sf.GetShippingMethod([ShippingMethod.Original))
                                               .Retours(nouveau OriginalShip()) ;
       IShippingCosting os = sf.GetShippingMethod(ShippingMethod.Original);
    
       dec = os.CalcMethodCost(prod, ShippingUrgency.Haute, .13m);
    
       Affirmer.AreEgal(145.5m, res); 
    }        
    

    Maintenant, il s'agit simplement de reproduire ce test pour chaque cas pertinent pour OriginalShip. Après cela, je répète le processus au fur et à mesure que je construis les autres objets de coût.

    En prévision de ces autres tests, il est logique pour moi de refactoriser mon code de test pour gagner du temps plus tard. J'ai coupé le code JustMock de mon premier test et créé une nouvelle méthode « d'assistance » de test qui générera des objets IProduct à la demande :

    private IProduct createMockProduct(string Id , 
                      			          int Poids,
             …plus de paramètres pertinents pour tester les frais d'expédition)
            {
                IProduct mockProduct = Mock.Créer<IProduct>();
                Mock.Arrange(() => mockProduct.prodId).[19659036]Retours(Id);
                Mock.Disposer(() => mockProduct.poids).[19659036]Returns(Weight);
                
    
                return mockProduct;
            }
    

    Maintenant, mes tests peuvent commencer par une ligne comme celle-ci :

    IProduct mckedProduct = CreateMockProduct("B456", 10);
    

    Troisième étape : écriture du niveau composant Tests

    Une fois que j'ai écrit suffisamment de tests unitaires pour me convaincre que tout fonctionne correctement avec mes objets de calcul, je vais passer au niveau des composants. Ici, je veux des tests pour ma nouvelle classe qui prouvent que tout mon code fonctionne ensemble pour fournir la réponse attendue par l'entreprise.

    Ce test est assez court et utilise un seul objet fictif avant d'appeler mon calculateur de frais d'expédition :

    [TestMethod]
    public void ShippingCostCalculatorTest()
    {
       IProduct mockedProduct = CreateMockProduct("C789", 1000);
    
       ShippingCostCalculator scc = new ShippingCostCalculator();
       decimal res =Sipping scc[1945369027].  (prod, 200, ShippingUrgency.High, ShippingMethod.Original ], .13m);
    
       J 'écrirai des tests supplémentaires pour différentes combinaisons d'objets de coût/produit. permettez-moi d'automatiser mes tests de régression. Une heure à une heure et demie de refactorisation me permet d'atteindre tous ces objectifs. 

    J'aurai écrit beaucoup de code de test en cours de route… mais vous ne pouvez pas éviter de tester : vous pouvez soit l'automatiser, soit le faire manuellement sur chaque version.

    L'automatisation de ces tests me donne une suite de tests de régression très puissante. Après avoir écrit ces tests, si j'apporte une modification à un objet de coût, je peux exécuter mes tests au niveau unitaire pour cet objet de coût pour m'assurer qu'il fonctionne toujours correctement. Si jamais j'apporte des modifications à CalcShipCost, je peux réexécuter mes tests au niveau des composants pour m'assurer que CalcShipCost fonctionne toujours correctement.

    En fait, pour any changer pour any de ces classes, ces tests automatisés prouveront que tous des scénarios de test fonctionnent toujours correctement. Cela élimine une tonne de tests de régression manuels. Et, lorsque j'exécuterai tous mes tests de régression automatisés en moins d'une minute, tout le monde se rendra compte que le code valait également la peine d'être écrit.




    Source link