Exporter (0) Imprimer
Développer tout

Instructions pour tester des solutions Azure de manière efficace

Mis à jour: juin 2014

Auteur : Suren Machiraju

Réviseurs : Jaime Alva Bravo et Steve Wilkins

Une fois votre solution Microsoft Azure créée, codée et déployée, il peut arriver qu'elle ne fonctionne pas. Cet article fournit des instructions sur la manière de tester vos applications Microsoft Azure tout au long du cycle de vie du développement logiciel. Le test inclut la logique métier et des tests de scénario de bout en bout complets. Cet article explique comment :

  • Développer des tests unitaires pour les composants de logique métier tout en éliminant les dépendances vis-à-vis des composants Microsoft Azure.

  • Concevoir des tests de bout en bout d'intégration.

  • Éliminer la surcharge pour les opérations de configuration, d'initialisation, de nettoyage et d'épuisement des ressources Microsoft Azure Service (par exemple, les files d'attente) pour chaque série de test.

  • Éliminer la création d'infrastructures en double pour les espaces de noms ACS, les files d'attente Service Bus et d'autres ressources.

Cet article offre également une vue d'ensemble des différentes technologies et techniques de test permettant de tester vos applications Microsoft Azure.

Cet article comporte les nouvelles modifications suivantes :

  1. Nous utilisons Visual Studio 2013.

  2. Microsoft Fakes dans Visual Studio 2013 remplace Moles. Cette fonctionnalité est décrite dans l'article « Isolation du code sous test avec Microsoft Fakes ».

  3. Dans Visual Studio 2013, une nouvelle version de la couverture du code est désormais disponible et peut être appelée directement à partir de l'Explorateur de test. Vous pouvez en lire une description dans l'article relatif à l'utilisation de la couverture du code pour déterminer la quantité de code testée.

  4. L'équipe Pex a publié une version légère de Pex intitulée Code Digger. Vous pouvez en lire une description dans la page relative à Microsoft Code Digger.

  5. À compter de Visual Studio 2012, nous déconseillons le test de méthodes privées. Une explication est disponible dans le billet de blog intitulé « My Take on Unit Testing Private Methods » (en anglais).

Il existe deux types de tests :

  • Les tests unitaires sont des tests étroitement ciblés, qui exercent une fonction unique donnée. Ils sont appelés « code sous test », ou CUT (Code Under Test). Toute dépendance requise par le CUT doit être supprimée.

  • Les tests d'intégration sont des tests plus larges qui exercent plusieurs fonctionnalités simultanément. Dans de nombreux cas, ils s'apparentent aux tests unitaires, mais ils couvrent différents domaines de fonctionnalité et incluent plusieurs dépendances.

Globalement, ces tests se consacrent à créer et utiliser des doubles de test. Les doubles de test suivants sont utilisés :

  • Les Fakes sont des objets simulés qui implémentent la même interface que l'objet qu'ils représentent. Ils renvoient des réponses prédéfinies. Un Fake contient un ensemble de stubs de méthode et sert de substitut pour ce que vous créez par programme.

  • Les Stubs simulent le comportement d'objets logiciels.

  • Les Shims vous permettent d'isoler votre code des assemblys qui ne font pas partie de votre solution. Ils isolent également les composants de votre solution les uns des autres.

Une fois exécutés, ces tests peuvent vérifier l'état et le comportement. Par exemple, l'état comprend le résultat suite à l'appel d'une méthode et le renvoi d'une valeur spécifique. L'appel d'une méthode dans un ordre donné et un nombre de fois donné est un exemple de comportement.

L'un des objectifs principaux des tests unitaires est d'éliminer les dépendances. Pour l'infrastructure Azure, ces dépendances incluent les éléments suivants :

  • Files d'attente du Service Bus

  • Service de contrôle d'accès

  • Cache.

  • Tables Azure, objets blob et files d'attente.

  • Base de données SQL Azure.

  • Azure Drive (anciennement Cloud Drive).

  • Autres Services Web.

Lors de la création de tests pour les applications Azure, nous remplaçons ces dépendances pour focaliser les tests sur l'exercice de la logique.

Les exemples de files d'attente de Service Bus (notamment les outils et techniques) présentés dans cet article s'appliquent également à toutes les autres dépendances.

Pour implémenter l'infrastructure de test pour vos applications Microsoft Azure, il vous faut :

  • une infrastructure de test unitaire pour définir et exécuter vos tests ;

  • une infrastructure mocking pour vous aider à isoler les dépendances et à créer des tests unitaires à portée étroite ;

  • des outils permettant de générer automatiquement des tests unitaires pour une plus grande couverture de code ;

  • d'autres infrastructures susceptibles d'aider à créer des conceptions testables en tirant parti de l'injection de dépendance et en appliquant le modèle Inversion de contrôle (IoC).

Visual Studio comprend un utilitaire de ligne de commande, appelé MS Test, qui exécute les tests unitaires créés dans Visual Studio. Visual Studio comprend également une suite de modèles de projet et d'élément pour prendre en charge les tests. Ainsi, lorsque vous créez un projet de test, vous ajoutez ensuite des classes (appelées éléments de test) dotées de l'attribut [TestClass]. Les classes contiennent des méthodes dotées de l'attribut [TestMethod]. Au sein de MS Test, différentes fenêtres dans Visual Studio vous permettent d'exécuter les tests unitaires définis dans le projet. Vous pouvez également examiner les résultats après exécution.

noteRemarque
MS Test n'est pas inclus dans les éditions de Visual Studio 2013 Express, Professional et Test Professional.

Dans MS Test, les tests unitaires suivent un modèle AAA (« Arrange, Act, Assert »).

  • Arrange : créer les objets, les configurations et tout autre élément requis et les entrées nécessaires au CUT.

  • Act : effectuer le test à portée étroite à proprement parler sur le code.

  • Assert : vérifier que les résultats attendus se sont bien produits.

Les bibliothèques de l'infrastructure de MS Test comprennent les classes d'assistance PrivateObject et PrivateType. Ces classes utilisent la réflexion pour faciliter l'appel de membres d'instance non publics ou de membres statiques depuis le code de test unitaire.

Les éditions Premium et Ultimate de Visual Studio incluent des outils de test unitaire améliorés. Ces outils sont intégrés à MS Test. Les outils vous permettent également d'analyser la quantité de code évaluée par vos tests unitaires. L'outil applique en outre un code de couleur au code source pour indiquer la progression du test. Cette fonctionnalité est appelée Couverture du code.

L'objectif du test unitaire est de tester de manière isolée. Cependant, le code soumis au test ne peut généralement pas être isolé. Il est possible que le code ne soit pas écrit pour pouvoir être testé. La réécriture du code serait difficile, car elle repose sur d'autres bibliothèques qui ne peuvent pas être facilement isolées. Par exemple, un code qui interagit avec des environnements externes ne peut pas être facilement isolé. Une infrastructure mocking permet d'isoler les deux types de dépendances.

Une liste d'infrastructures mocking envisageables est disponible dans la section des liens utiles située à la fin de cet article. Le présent article est consacré à l'utilisation de Microsoft Fakes.

Microsoft Fakes vous permet d'isoler le code testé en remplaçant d'autres parties de l'application par des stubs ou des shims. Il s'agit de petits éléments de code qui sont contrôlés par vos tests. En isolant votre code en vue de le tester, vous êtes sûr que la cause d'un éventuel échec du test se trouve ici et pas ailleurs. Les stubs et shims vous permettent également de tester votre code même si d'autres parties de votre application ne fonctionnent pas encore.

Les Fakes se présentent sous deux formes différentes :

  • Un stub remplace une classe par un petit substitut qui implémente la même interface. Pour utiliser des stubs, vous devez concevoir votre application de sorte que chaque composant dépende uniquement des interfaces et non d'autres composants. Nous entendons par « composant » une classe ou un groupe de classes conçues et mises à jour ensemble et généralement contenues dans un assembly.

  • Un shim modifie le code compilé de votre application au moment de l'exécution. Au lieu d'effectuer un appel de méthode spécifié, votre application exécute le code shim fourni par votre test. Vous pouvez utiliser des shims pour remplacer des appels d'assemblys que vous ne pouvez pas modifier, comme les assemblys .NET.

Code Digger analyse les chemins d'exécution possibles dans votre code .NET. Le résultat se présente sous forme de tableau dont chaque ligne présente un comportement unique de votre code. Le tableau permet de mieux comprendre le comportement du code, et éventuellement de déceler des bogues cachés.

Pour analyser votre code dans l'éditeur Visual Studio, utilisez le nouvel élément de menu contextuel Générer une table des entrées/sorties pour appeler Code Digger. Code Digger calcule et affiche les paires entrée-sortie. Code Digger recherche systématiquement les bogues, les exceptions et les échecs d'assertion.

Par défaut, Code Digger fonctionne uniquement sur le code .NET public qui réside dans des bibliothèques de classes portables. Nous aborderons la procédure de configuration de Code Digger afin d'explorer d'autres projets .NET plus loin dans cet article.

Code Digger utilise le moteur Pex et le solveur de contrainte Z3 de Microsoft Research pour analyser toutes les branches du code de manière systématique. Code Digger tente de générer une suite de tests qui couvre une grande quantité de code.

Vous pouvez utiliser Microsoft Unity pour les conteneurs injection de dépendance (DI) et inversion de contrôle (IoC) extensibles. L'application prend en charge l'interception, l'injection du constructeur, l'injection de propriété et l'injection d'appel de méthode. Microsoft Unity et les outils apparentés vous aident à créer des conceptions de test permettant l'insertion de vos dépendances à tous les niveaux de votre application. (Nous considérons que votre application a été conçue et créée en tenant compte de l'injection de dépendance et de l'une de ces infrastructures.)

Ces infrastructures sont idéales pour écrire du code pouvant être testé, autrement dit, du code efficace. Elles peuvent cependant impliquer certaines exigences en termes de conception initiale. Les conteneurs injection de dépendance et inversion de contrôle ne sont pas abordés dans cet article.

Dans cette section, nous décrivons une solution qui inclut un site web hébergé sur un rôle Web. Le site web transmet les messages à une file d'attente. Un rôle de travail traite ensuite les messages provenant de la file d'attente. Nous voulons tester certains aspects de ces trois éléments.

Imaginez que vous possédez un site web qui crée des commandes, et une file d'attente Service Bus crée une file d'attente pour traiter ces commandes. La page web ressemble à la Figure 1 :

Figure 1

Lorsque l'utilisateur clique sur Créer, la file d'attente Service Bus place la nouvelle commande sur l'action Create du contrôleur associé. Cette action est implémentée comme suit :

noteRemarque
La classe MicrosoftAzureQueue est une classe wrapper qui utilise les API .NET Service Bus (telles que MessageSender et MessageReceiver) pour interagir avec les files d'attente Service Bus.

private IMicrosoftAzureQueue queue;
public OrderController()
{
    queue = new MicrosoftAzureQueue();
}
public OrderController(IMicrosoftAzureQueue queue)
{
    this.queue = queue;
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "OrderId,Description")] Order order)
{
    try
    {
        if (ModelState.IsValid)
        {
            string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
            string queueName = "ProcessingQueue";
            queue.InitializeFromConnectionString(connectionString, queueName);
            queue.Send(order);
        }
        return View("OrderCreated");
    }
    catch (Exception ex)
    {
        Trace.TraceError(ex.Message);
        return View("Error");
    }            
}

Vous constatez que le code récupère les paramètres de configuration auprès de CloudConfigurationManager et envoie un message contenant la commande à mettre en file d'attente. Vous remarquez également que l'action Create utilise les méthodes suivantes :

  • InitializeFromConnectionString (chaîne ConnectionString, chaîne QueueName)

  • Send (classe MicrosoftAzureQueue)

Nous voulons créer des détournements pour ces méthodes à l'aide de Fakes pour contrôler leur comportement et supprimer les dépendances à l'environnement réel. L'utilisation de Fakes évite d'avoir à exécuter les tests dans l'émulateur Azure ou à appeler la file d'attente Service Bus. Nous exécutons l'action Create sur le contrôleur pour vérifier que l'entrée Commande est bien celle envoyée à la file d'attente. La méthode Send vérifie que les éléments ID de commande et Description sont bien indiqués comme entrées de l'action. Elle vérifie ensuite que la vue OrderCreated apparaît comme résultat.

Il est assez simple de créer un test unitaire pour l'implémentation ci-dessus à l'aide de Fakes. Dans le projet de test, cliquez avec le bouton droit sur l'assembly qui contient les types à mocker. Sélectionnez ensuite Ajouter un assembly Fakes.

Dans l'exemple, nous sélectionnons Microsoft.ServiceBus, puis Ajouter un assembly Fakes. Un fichier XML intitulé « Microsoft.ServiceBus.Fakes » est ajouté au projet de test. Répétez l'action pour l'assembly Microsoft.WindowsAzure.Configuration.

Lors de la création du projet de test, des références sont ajoutées aux versions générées automatiquement de ces assemblys. Dans l'exemple, les assemblys générés sont « Microsoft.ServiceBus.Fakes » et « Microsoft.WindowsAzure.Configuration ».

Créez une méthode de test unitaire et appliquez l'attribut [TestCategory("With fakes")]. Dans le test unitaire, utilisez des shims pour isoler les parties de votre application les unes des autres.

Utilisation de Shims pour isoler votre application d'autres assemblys en vue d'effectuer un test unitaire

Les shims constituent l'une des deux technologies fournies par Microsoft Fakes Framework qui permettent d'isoler facilement de l'environnement des composants soumis au test. Les shims détournent les appels à des méthodes spécifiques vers le code que vous écrivez dans le cadre de votre test. De nombreuses méthodes renvoient différents résultats en fonction des conditions externes. En revanche, un shim est contrôlé par votre test et peut renvoyer des résultats cohérents à chaque appel. Vos tests son ainsi considérablement plus faciles à écrire. Utilisez des shims pour isoler votre code des assemblys qui ne font pas partie de votre solution. Pour isoler des composants de votre solution les uns des autres, nous vous recommandons d'utiliser des stubs.

Utilisation de stubs pour isoler des parties de votre application les unes des autres en vue d'effectuer un test unitaire

Les stubs constituent l'une des deux technologies fournies par Microsoft Fakes Framework qui permettent d'isoler facilement un composant soumis au test d'autres composants qu'il appelle. Un stub est un petit élément de code qui remplace un autre composant pendant le test. L'utilisation d'un stub présente l'avantage de renvoyer des résultats cohérents, ce qui facilite l'écriture du test. De plus, vous pouvez exécuter des tests même si les autres composants ne fonctionnent pas encore.

Dans notre exemple de test, nous allons utiliser des shims pour les assemblys CloudConfigurationManager et BrokeredMessage d'Azure. Nous allons utiliser des stubs pour MicrosoftAzureQueue, qui est une classe de notre solution.

[TestMethod]
[TestCategory("With fakes")]
public void Test_Home_CreateOrder()
{
    // Shims can be used only in a ShimsContext
    using (ShimsContext.Create())
    {
        // Arrange
        // Use shim for CloudConfigurationManager.GetSetting
        Microsoft.WindowsAzure.Fakes.ShimCloudConfigurationManager.GetSettingString = (key) =>
        {
            return "mockedSettingValue";
        };
                
        // Create the fake queue:
        // In the completed application, queue would be a real one:
        bool wasCreateFromConnString = false;
        Order orderSent = null;
        IMicrosoftAzureQueue queue =
                new OrderWebRole.Queue.Fakes.StubIMicrosoftAzureQueue() // Generated by Fakes.
                {
                    // Define each method:
                    // Name is original name + parameter types:
                    InitializeFromConnectionStringStringString = (connectionString, queueName) => {
                        wasCreateFromConnString = true;
                    },
                    SendOrder = (order) => {
                    orderSent = order;
                    }
                };

        // Component under test
        OrderController controller = new OrderController(queue);

        // Act
        Order inputOrder = new Order()
        {
            OrderId = System.Guid.NewGuid(),
            Description = "A mock order"
        };
        ViewResult result = controller.Create(inputOrder) as ViewResult;

        //Assert
        Assert.IsTrue(wasCreateFromConnString);
        Assert.AreEqual("OrderCreated", result.ViewName);
        Assert.IsNotNull(orderSent);
        Assert.AreEqual(inputOrder.OrderId, orderSent.OrderId);
        Assert.AreEqual(inputOrder.Description, orderSent.Description);
    }
}

Le rôle Web se charge d'ajouter une commande à la file d'attente. Vous devez maintenant envisager une manière de tester un rôle de travail qui traite les commandes en les récupérant dans la file d'attente Service Bus. La méthode Run de notre rôle de travail interroge régulièrement la file d'attente à la recherche de commandes, pour ensuite les traiter.

private IMicrosoftAzureQueue queue;
public WorkerRole()
{
    queue = new MicrosoftAzureQueue();
}

public WorkerRole(IMicrosoftAzureQueue queue)
{
    this.queue = queue;
}
public override void Run()
{
    try
    {
        string connectionString = CloudConfigurationManager.GetSetting("Microsoft.ServiceBus.ConnectionString");
        string queueName = "ProcessingQueue";
               
        queue.InitializeFromConnectionString(connectionString, queueName);
            
        queue.CreateQueueIfNotExists();
              
        while (true)
        {
            Thread.Sleep(2000);
            //Retrieve order from Service Bus Queue  
            TryProcessOrder(queue);
        }
    }
    catch (Exception ex)
    {
        if (queue != null)
            queue.Close();
        System.Diagnostics.Trace.TraceError(ex.Message);
    }
}

Nous voulons créer un test pour vérifier que la routine récupère correctement un message. Voici le test unitaire complet pour la méthode Run du rôle de travail.

[TestMethod]
public void Test_WorkerRole_Run()
{
    // Shims can be used only in a ShimsContext:
    using (ShimsContext.Create())
    {
        Microsoft.WindowsAzure.Fakes.ShimCloudConfigurationManager.GetSettingString = (key) =>
        {
            return "mockedSettingValue";
        };

        // Arrange 
        bool wasEnsureQueueExistsCalled = false;
        int numCallsToEnsureQueueExists = 0;

        // Create the fake queue:
        // In the completed application, queue would be a real one:
        bool wasConnectionClosedCalled = false;
        bool wasCreateFromConnString = false;
        bool wasReceiveCalled = false;
        int numCallsToReceive = 0;

        bool wasCompleteCalled = false;
        int numCallsToComplete = 0;
        IMicrosoftAzureQueue queue =
                new OrderWebRole.Queue.Fakes.StubIMicrosoftAzureQueue() // Generated by Fakes.
                {

                    // Define each method:
                    // Name is original name + parameter types:
                    InitializeFromConnectionStringStringString = (connectionString, queueName) =>
                    {
                        wasCreateFromConnString = true;
                    },
                    CreateQueueIfNotExists = () =>
                    {
                        wasEnsureQueueExistsCalled = true;
                        numCallsToEnsureQueueExists++;
                    },
                    Receive = () =>
                {
                    wasReceiveCalled = true;
                    if (numCallsToReceive >= 3) throw new Exception("Aborting Run");
                    numCallsToReceive++;
                    Order inputOrder = new Order()
                    {
                        OrderId = System.Guid.NewGuid(),
                        Description = "A mock order"
                    };
                    return new BrokeredMessage(inputOrder);
                },
                    Close = () =>
                    {
                        wasConnectionClosedCalled = true;
                    }

                };


        Microsoft.ServiceBus.Messaging.Fakes.ShimBrokeredMessage.AllInstances.Complete = (message) =>
        {
            wasCompleteCalled = true;
            numCallsToComplete++;
        };

        WorkerRole workerRole = new WorkerRole(queue);

        //Act
        workerRole.Run();

        //Assert
        Assert.IsTrue(wasCreateFromConnString);
        Assert.IsTrue(wasConnectionClosedCalled);
        Assert.IsTrue(wasEnsureQueueExistsCalled);
        Assert.IsTrue(wasReceiveCalled);
        Assert.AreEqual(1, numCallsToEnsureQueueExists);
        Assert.IsTrue(numCallsToReceive > 0);
        Assert.IsTrue(wasCompleteCalled);
        Assert.IsTrue(numCallsToComplete > 0);
        Assert.AreEqual(numCallsToReceive, numCallsToComplete);

    }
}

Vous devez définir le délégué de la propriété AllInstances du type Fake généré. En utilisant le délégué, toute instance du type Fake réel que vous créez est détournée via l'une des méthodes pour laquelle vous avez défini des délégués.

Dans l'exemple, nous voulons utiliser la méthode Run de l'instance d'origine, mais également fournir des détournements pour les méthodes d'instance CreateQueue et TryProcessOrder. Dans le code, nous levons une exception de manière à quitter la boucle infinie gérée par la méthode Run à un moment prédéterminé.

Vous vous demandez peut-être pourquoi nous n'utilisons pas simplement MessageSender/MessageReceiver et les classes associées directement à partir du kit de développement logiciel Service Bus au lieu d'insérer un type d'assistant. Pour isoler le code de sorte qu'il n'appelle pas le Service Bus réel, il existe deux possibilités :

  • écrire des Fakes qui héritent des classes abstraites dans l'espace de noms Microsoft.ServiceBus ;

  • laisser les Fakes créer des types mock pour tous.

Pour les deux approches, la complexité pose problème. Quelle que soit la méthode utilisée, vous finissez par créer une réflexion dans des classes telles que TokenProvider et QueueClient. La réflexion entraîne les problèmes suivants :

  • Vous devez créer des types dérivés de ces types abstraits qui exposent toutes les substitutions nécessaires.

  • Vous devez exposer les types internes sur lesquels reposent les versions réelles de ces classes.

  • Pour les types internes, vous devez recréer leurs constructeurs ou méthodes d'usine de manière judicieuse pour supprimer la dépendance sur le Service Bus réel.

La meilleure option consiste à insérer votre propre type d'assistant. C'est tout ce dont vous avez besoin pour mocker, détourner et isoler le code du Service Bus réel.

Pour analyser ce que ces tests unitaires ont vérifié, nous pouvons examiner les données de la couverture du code. Si nous exécutons les deux tests unitaires à l'aide de MS Test, nous constatons qu'ils réussissent. Nous observons également les détails d'exécution associés issus de la boîte de dialogue Explorateur de test.

Figure 2

Vous pouvez exécuter la couverture du code pour vos tests à partir de l'Explorateur de test. Cliquez avec le bouton droit sur le test et sélectionnez Analyser la couverture du code pour les tests sélectionnés. Les résultats apparaissent dans la fenêtre Résultats de la couverture du code. Pour activer la collecte de données pour la couverture du code, configurez le fichier Local.testsettings sous les Éléments de solution. L'ouverture de ce fichier déclenche l'éditeur Paramètres de test.

Si votre solution ne comporte pas de fichier Local.testsettings, ajoutez-en un en procédant comme suit.

  1. Cliquez sur le bouton Ajouter un nouvel élément.

  2. Sélectionnez Test, puis cliquez sur Paramètres de test.


    Figure 3

  3. Sous l'onglet Données et diagnostics, cochez la case située à droite de la ligne Couverture du code.

  4. Cliquez ensuite sur le bouton Configurer .

  5. Dans la fenêtre Détails de la couverture du code, sélectionnez tous les assemblys à tester et cliquez sur OK.


    Figure 4

  6. Pour faire disparaître l'éditeur Paramètres de test, cliquez sur Appliquer et fermer.

  7. Réexécutez vos tests, puis cliquez sur le bouton Couverture du code. Vos résultats de couverture du code doivent apparaître comme illustré dans la Figure 5.


    Figure 5

  8. Cliquez sur l'icône Afficher la coloration de la couverture du code, puis accédez à une méthode dans la grille.

  9. Double-cliquez sur la méthode. Son code source s'affichera en couleur, indiquant que les zones ont été testées. Une couleur verte indique que le code a été testé. Une couleur grise indique que le code a été partiellement testé ou non testé.


    Figure 6

Il est utile de créer manuellement des tests unitaires, mais Code Digger vous permet également de les mettre à niveau de manière efficace. Il teste pour cela des valeurs de paramètre auxquelles vous n'avez peut-être pas pensé. Une fois Code Digger installé, pour explorer une méthode, cliquez dessus avec le bouton droit dans l'éditeur de code et sélectionnez Générer un table des entrées/sorties.

Figure 7

Au bout de quelques instants, Code Digger termine le processus et les résultats se stabilisent. Par exemple, la Figure 8 présente les résultats de l'exécution de Code Digger sur la méthode TryProcessOrder du rôle de travail. Vous constatez que Code Digger a pu créer un test qui a donné une exception. Plus important encore, Code Digger indique les entrées qu'il a créées pour générer cette exception (une valeur null pour le paramètre de file d'attente).

Figure 8

Afficher:
© 2014 Microsoft