Fermer

octobre 19, 2021

Test unitaire des fonctions Azure dans un environnement isolé


Voyons une étude de cas sur la façon dont JustMock vous permet d'automatiser les tests pour une architecture de microservices typique (y compris en se moquant de Entity Framework).

Même aujourd'hui, les études de cas utilisées dans les exemples de tests automatisés ignorent généralement à la fois l'environnement cloud et l'environnement moderne. architectures de microservices. Ainsi, cet article examinera comment JustMock vous permet d'automatiser les tests pour une architecture de microservice typique (y compris en moquant Entity Framework).

Pour mon étude de cas, je vais implémenter le modèle fournisseur/consommateur. — c'est un modèle populaire qui offre à la fois une évolutivité et une fiabilité élevées. Cette implémentation du modèle utilise une interface API Web (le fournisseur) pour accepter les demandes et un backend Azure Function (le consommateur) pour traiter ces demandes. Le fournisseur écrit des demandes dans une file d'attente de stockage Azure et le consommateur lit et traite ces demandes au fur et à mesure qu'elles apparaissent dans la file d'attente. une file d'attente de stockage (le client récupère juste un code d'état HTTP 202 Accepté). Cela ne prend pratiquement pas de temps, de sorte que cette application d'API Web peut gérer un très grand nombre de demandes dans n'importe quelle période.

Dans .NET Core (ou, vraiment, toute version de l'API Web ASP.NET), cela une partie de mon microservice Azure ressemblerait à ceci :

[HttpPost]
public IActionResult Post(SalesOrder so)[19659010]{

   

   string jsonSO = JsonSerializer.Serialize<SalesOrder>(so);
   QueueClient qc = new QueueClient(
     new Uri("…URI pour la file d'attente…"),[19659018]new StorageSharedKeyCredential("…nom du compte", "…clé cryptée laide…"));
   qc.EnvoyerMessage(jsonSO);
   retour Accepté(so);[19659010]}

La fonction Azure qui agit en tant que consommateur dans cette implémentation du modèle est câblée pour s'exécuter à chaque fois qu'un message apparaît dans la file d'attente et reçoit automatiquement le corps du message. La fonction prendra plus de temps pour traiter le SalesOrder qu'elle lit dans la file d'attente que le service Web n'en a pris pour écrire le SalesOrder dans la file d'attente. Entre autres tâches, le consommateur va gérer toutes les mises à jour de la base de données à l'aide d'Entity Framework.

Aux heures de pointe, le consommateur prendra du retard et le nombre de commandes clients en file d'attente, en attente de traitement, augmentera. Au fil du temps, cependant, lorsque la demande sur ce microservice diminue, le consommateur rattrapera son retard et finira par effacer la file d'attente (autrement, je pourrais démarrer une autre copie du consommateur pour effacer la file d'attente plus rapidement, améliorant encore l'évolutivité). Si le consommateur échoue, les messages resteront dans la file d'attente jusqu'à ce que le consommateur soit redémarré, offrant un degré élevé de fiabilité.

Lorsqu'un message apparaît dans la file d'attente, Azure transmet le corps du message et une référence à un outil de journalisation à la fonction. Une fonction Azure qui traite ce message et, éventuellement, l'ajoute en tant que SalesOrder à la base de données, ressemblerait à ceci :

[FunctionName("SalesOrderProcesser")[19659009]]
 public static void Exécuter(
   [QueueTrigger("salesorderprocessing", Connexion = "salesorder")] string myQueueItem, ILogger log)
{
    try
    {
       SalesOrder donc = JsonSerializer.Deserialize<SalesOrder>(myQueueItem); 
       log.LogInformation($"C# Fonction de déclenchement de file d'attente traitée : {so.SalesOrderId}");

       

       SalesDb db = nouveau SalesDb();
       db.SalesOrders.Ajouter(so);
       db.Enregistrer les modifications();
   }
   catch (Exception e)
   {
      log.LogError($"Erreur de traitement : {e.Message}");
   }
}

Ma première étape dans la création de tests automatisés pour ces composants dans Visual Studio consiste à ajouter un projet de test à la solution qui les contient, à l'aide du modèle de projet installé avec JustMock. Une fois que cela est en place, je peux commencer à écrire mes tests. , alors tout en aval du fournisseur va échouer.

Pour démarrer le test, j'ai besoin d'un objet SalesOrder fictif à transmettre à la méthode Get de mon service Web. Le code à créer est assez simple :

public void SalesOrderProviderWriteTest()
{
   SalesOrder donc = nouveau SalesOrder
   {
      SalesOrderId = 0,
      CustomerId = 112,
      
   };    

Ensuite, je dois me moquer de la méthode SendMessage de QueueClient afin de pouvoir capturer ce qui est écrit dans cette file d'attente. Tout d'abord, j'instancie un objet QueueClient à utiliser lors de la configuration de ma méthode fictive SendMessage. Je ne vais pas utiliser ce QueueClient pour autre chose que pour configurer mes méthodes fictives, je lui transmets donc des valeurs factices, comme ceci :

QueueClient qc = new QueueClient (nouveau Uri("http://phv.com"),
    nouveau StorageSharedKeyCredential("xxx ", "bcd=")
  );

Avec cela en place, j'utilise la classe Mock de JustMock pour fournir une version fictive de la méthode SendMessage qui, au lieu de écrit dans la file d'attente, met à jour une variable dans mon test. Je peux ensuite vérifier ce qui se termine dans cette variable dans mes instructions Assert à la fin du test pour voir si le fournisseur a fait la bonne chose.

Après avoir configuré ma variable, il y a trois étapes pour se moquer de SendMessage (bien que, merci à l'interface fluide de JustMock, tout se résume dans une seule déclaration).

Première étape: en utilisant la méthode Arrange sur la classe Mock de JustMock, j'appelle la méthode SendMessage que je veux me moquer de l'objet QueueClient que j'utilise comme base. La méthode SendMessage attend un paramètre de chaîne, j'utilise donc l'un des matchers de JustMock, Arg.IsAny() dans ce cas, pour satisfaire SendMessage.

Deuxième étape : ce n'est pas l'objet QueueClient utilisé dans la méthode Get de mon fournisseur ( cette méthode crée son propre objet QueueClient). Cependant, si j'utilise la méthode IgnoreInstance de JustMock, toute moquerie que je fais à l'objet que je crée ici sera également appliquée à toute instance de QueueClient créée dans le cadre de mon test.

Troisième étape : j'utilise la méthode DoInstead de JustMock pour fournir un remplacement de la vraie méthode SendMessage. Mon remplacement est une expression lambda qui, comme SendMessage, accepte un paramètre de chaîne. Plutôt que d'écrire ce message dans une file d'attente, mon expression lambda utilise ce paramètre pour mettre à jour la variable dans mon code de test.

Voici le code qui définit la variable et configure la méthode fictive qui met à jour cette variable :

string  écritSO = chaîne.Vide;
Mock.Arrange(() => qc.SendMessage( Arg.EstTout<chaîne>() ))
  .IgnorerInstance( )
  .DoInstead((string jSO) => écritSO = jSO);

Maintenant que j'ai pris le contrôle de ma méthode Provider, je peux lancer mon test. J'instancie le contrôleur de l'API Web qui contient ma méthode et appelle la méthode en passant mon objet SalesOrder factice :

SalesOrderProvider sop = new SalesOrderProvider() ; 
IActionResult res = sop.Post(so);

En raison de mon code JustMock, la version JSON de l'objet SalesOrder normalement écrit dans une file d'attente sera maintenant mis dans ma variable writeSO où je peux le vérifier. Ma première assertion est donc de voir si cette variable est en fait un objet SalesOrder : >(écritSO);
Assert.IsNotNull(writeSalesOrder);

En résumé, le test ressemble à ceci :

public void[19659037]EcrireSalesOrderTest()
{
    SalesOrder donc = nouveau SalesOrder
    {
        SalesOrderId = 0,
        ID client = 112,
        Date de commande = DateHeure.Maintenant,
        SalesOrderNumber = "A123",
        Date d'expédition = DateHeure.Maintenant.Ajouter des jours(4),
        Valeur totale = 293.76m
    };

    QueueClient qc = nouveau QueueClient(nouveau Uri("http://phv.com")
      new StorageSharedKeyCredential("xxx", "bcd=")
    );

    string écrit SO = chaîne.Vide;
    Mock.Organiser(() => qc.SendMessage(Arg.IsAny<string>()))
      .IgnoreInstance( )
      .DoInstead((string jSO) => écritSO = jSO);
    SalesOrderProvider sop = nouveau SalesOrderProvider();

    IActionResult res = sop.Post(so);

    SalesOrder wroteSalesOrder = JsonSerializer.Deserialize<SalesOrder>(writeSO;[19659009
    Assert.IsNotNull(writtenSalesOrder);
}

Mes tests ne s'arrêteraient pas là dans la vraie vie, bien sûr. Par exemple, je vérifierais probablement que ce SalesOrder a les mêmes valeurs de propriété que l'objet SalesOrder que j'ai passé dans la méthode Get ; Je créerais également des tests avec divers ordres de vente « invalides » pour m'assurer que le fournisseur fait la bonne chose avec eux. Et je ferais un test sans me moquer de SendMessage pour m'assurer que je peux écrire avec succès dans la file d'attente.

Mais, à la place, regardons comment je peux automatiser les tests pour la partie consommateur de ce modèle.

Test du consommateur

Les fonctions Azure sont configurées en tant que méthodes statiques sur une classe. Par conséquent, pour appeler ma fonction Azure depuis un test, il me suffit d'appeler la fonction depuis la classe, en passant les deux paramètres attendus par la méthode : la partie message de l'objet file d'attente qui a déclenché la fonction (dans ce cas, une version JSON d'un objet SalesOrder) et un objet qui implémente l'interface ILogger.

Je vais créer le SalesOrder à transmettre à la fonction comme je l'ai fait avec le fournisseur, mais j'ajouterai une ligne supplémentaire pour convertir mon SalesOrder vers JSON :

string jsonSO = JsonSerializer.Serialize(so);

Avec mon message fictif créé, je peux créer mon faux logger. Avec JustMock, selon ce que je veux tester sur le logger, j'ai plusieurs manières de le moquer. Je pourrais, par exemple, suivre le nombre de fois qu'une méthode de l'enregistreur est appelée ou capturer les messages envoyés à ces méthodes.

Je vais cependant commencer par deux tests plus simples : si le code s'exécute comme prévu, le La méthode LogInformation d'ILogger doit être appelée exactement une fois et la méthode LogError ne doit jamais être appelée (encore une fois, si tout se passe bien).

Pour vérifier ces conditions, j'utilise d'abord la méthode Create de la classe Mock pour générer un objet ILogger fictif à utiliser comme la base pour se moquer des méthodes LogInformation et LogError. J'utilise ensuite la méthode Arrange de la classe Mock pour spécifier que je m'attends à ce que la méthode LogInformation soit appelée une fois et que la LogError ne soit jamais appelée. ]= Mock.Créer<ILogger>();
Mock.Organiser(() => mockLog.LogInformation(Arg.TouteChaîne)).OccursOnce();
Mock.Arrange(() => mockLog.LogError(Arg.AnyString)).OccursNever();

Mocking Database Access

Alors que j'ai maintenant les paramètres pour appeler ma fonction Azure, je souhaite également prendre le contrôle du code Entity Framework à l'intérieur de la fonction afin de pouvoir vérifier que les bonnes données vont dans la base de données (de plus, je ne veux pas que mes tests ajoutent ces éléments factices commandes dans ma base de données). Cela ne nécessite que trois étapes.

Ma première étape consiste à créer une collection factice à utiliser par mon objet DbContext à la place de la vraie base de données :

IList<SalesOrder> mockSOs = new List<SalesOrder>();           

Ma deuxième étape consiste à créer une instance de l'objet DbContext utilisé par la fonction. Une fois que je l'ai créé, la classe Mock renvoie ma collection factice chaque fois que la fonction utilise la collection SalesOrders. J'utilise à nouveau IgnoreInstance ici car l'objet DbContext que j'utilise pour créer mes maquettes n'est pas celui que ma fonction utilise.

Voici ce code :

SalesDb db = new  SalesDb();
Mock.Organiser(() => db.SalesOrders).[19659033]IgnorerInstance()
                                                                .ReturnsCollection<SalesOrder>(mockSOs); 

La troisième étape consiste à adresser les deux méthodes qui sont appelées à l'intérieur de la fonction : la méthode Add sur la collection SalesOrders et la méthode SaveChanges sur l'objet DbContext lui-même.

La méthode SaveChanges est facile à simuler : j'utilise le Classe fictive pour dire à SaveChanges de ne rien faire. Pour la méthode Add, j'aurai Mock swap dans un nouvel ensemble de code qui ajoute l'objet SalesOrder passé à la méthode Add à ma collection factice, plutôt qu'à la base de données.

Voici ce code :

 Mock.Réorganiser(() => db.Enregistrer les modifications() ).Ne rien faire();
   Mock.Organiser(() => db.SalesOrders.Ajouter[19659009](Arg.IsAny<SalesOrder>()))
      . IgnorerInstance()
      .DoInstead((SalesOrder addSO) => mockSOs.Add(addSO));

Avec tous ces arrangements terminés, je suis prêt à appeler ma fonction, en passant mon faux SalesOrder et enregistreur. Une fois l'exécution de la méthode terminée, je vérifierai si quelque chose s'est retrouvé dans ma collection factice :

SalesOrderConsumer.SalesProcesser.Run(jsonSO , mockLog);
Affirmer.AreEgal(1, mockSOs.Count());[19659041] Lorsque ce test est assemblé, il ressemblera à ceci :

[TestMethod]
public void ReadTest()[19659010]{
    ILogger mockLog = Mock.Créer<ILogger>();
    Mock.Organiser(() => mockLog.LogInformation(Arg.TouteChaîne)).OccursOnce();
    Mock.Arrange(() => mockLog.LogError(Arg.N'importe quelle chaîne)).Never();

    IList<SalesOrder> mockSOs = new List<SalesOrder>() 19659009];
    SalesDb db = new SalesDb();
    Mock.Organiser(() => db.SalesOrders)
      .[19659033]IgnorerInstance()
      .ReturnsCollection<SalesOrder>(mockSOs); 
    Mock.Organiser(() => db.Enregistrer les modifications()[19659009])
      .Ne rien faire();
    Mock.Organiser(() => db.SalesOrders.Ajouter[19659009](Arg.IsAny<SalesOrder>()))
      . IgnorerInstance()
      .DoInstead((SalesOrder addSo) => mockSOs.Ajouter(ajouterSo));

    SalesOrder donc = nouveau SalesOrder
    {
        SalesOrderId = 0,
        ID client = 112,
        Date de commande = DateHeure.Maintenant,
        SalesOrderNumber = "A123",
        Date d'expédition = DateHeure.Maintenant.Ajouter des jours(4),
        Valeur totale = 293.76m
    };
    string jsonSO = JsonSerializer.Serialize(so);

    SalesProcesser.Run(jsonSO, mockLog);

    Affirmer.AreEgal(1, mockSOs.Count());[19659041] Dans mon test réel, bien sûr, je vérifierais s'il y avait plus qu'un seul objet dans ma collection factice. 

Je peux maintenant vérifier tout cela dans le contrôle de source et ajouter mes tests à mon exécution de génération nocturne. Ma prochaine étape consiste à commencer à réfléchir à la façon dont je vais écrire les tests d'intégration qui prouveront que tout ce code fonctionne ensemble après son chargement dans Azure (j'utiliserai probablement Test Studio for APIs pour cela) . Mais c'est un autre article de blog.




Source link