Partager via


Activer les tests unitaires automatisés

par Microsoft

Télécharger le PDF

Il s’agit de l’étape 12 d’un didacticiel gratuit sur l’application « NerdDinner » qui explique comment créer une petite application web, mais complète, à l’aide de ASP.NET MVC 1.

L’étape 12 montre comment développer une suite de tests unitaires automatisés qui vérifient nos fonctionnalités NerdDinner et qui nous donneront la confiance nécessaire pour apporter des modifications et des améliorations à l’application à l’avenir.

Si vous utilisez ASP.NET MVC 3, nous vous recommandons de suivre les didacticiels Prise en main Avec MVC 3 ou MVC Music Store.

NerdDinner Étape 12 : Tests unitaires

Nous allons développer une suite de tests unitaires automatisés qui vérifient nos fonctionnalités NerdDinner et qui nous donneront la confiance nécessaire pour apporter des modifications et des améliorations à l’application à l’avenir.

Pourquoi un test unitaire ?

Sur le lecteur de travail un matin, vous avez un éclair soudain d’inspiration sur une application sur laquelle vous travaillez. Vous réalisez qu’il existe un changement que vous pouvez implémenter pour améliorer considérablement l’application. Il peut s’agir d’une refactorisation qui nettoie le code, ajoute une nouvelle fonctionnalité ou corrige un bogue.

La question qui vous est posée lorsque vous arrivez à votre ordinateur est : « dans quelles cas est-il sûr d’apporter cette amélioration ? » Que se passe-t-il si le changement a des effets secondaires ou interrompt quelque chose ? La modification peut être simple et ne prendre que quelques minutes à implémenter, mais que se passe-t-il si le test manuel de tous les scénarios d’application prend des heures ? Que se passe-t-il si vous oubliez de couvrir un scénario et qu’une application défaillante est mise en production ? Est-ce que faire cette amélioration en vaut vraiment la peine ?

Les tests unitaires automatisés peuvent fournir un filet de sécurité qui vous permet d’améliorer continuellement vos applications et d’éviter d’avoir peur du code sur lequel vous travaillez. Le fait d’avoir des tests automatisés qui vérifient rapidement les fonctionnalités vous permet de coder en toute confiance et vous permet d’apporter des améliorations que vous n’auriez peut-être pas senti à l’aise de faire. Ils aident également à créer des solutions plus maintenables et ayant une durée de vie plus longue, ce qui entraîne un retour sur investissement beaucoup plus élevé.

L’infrastructure ASP.NET MVC rend facile et naturelle la fonctionnalité d’application de test unitaire. Il active également un workflow de développement piloté par les tests (TDD) qui permet un développement basé sur le test d’abord.

Projet NerdDinner.Tests

Lorsque nous avons créé notre application NerdDinner au début de ce didacticiel, une boîte de dialogue nous a demandé si nous voulions créer un projet de test unitaire pour l’accompagner dans le projet d’application :

Capture d’écran de la boîte de dialogue Créer un projet de test unitaire. Oui, l’option Créer un projet de test unitaire est sélectionnée. Nerd Dinner dot Tests est écrit en tant que nom du projet Test.

Nous avons conservé la case d’option « Oui, créer un projet de test unitaire » sélectionnée, ce qui a entraîné l’ajout d’un projet « NerdDinner.Tests » à notre solution :

Capture d’écran de l’arborescence de navigation Explorateur de solutions. Nerd Dinner Dot Tests est sélectionné.

Le projet NerdDinner.Tests fait référence à l’assembly de projet d’application NerdDinner et nous permet d’y ajouter facilement des tests automatisés qui vérifient les fonctionnalités de l’application.

Création de tests unitaires pour notre classe de modèle Dinner

Nous allons ajouter des tests à notre projet NerdDinner.Tests qui vérifient la classe Dinner que nous avons créée lorsque nous avons créé notre couche de modèle.

Nous allons commencer par créer un dossier dans notre projet de test appelé « Modèles » dans lequel nous allons placer nos tests liés aux modèles. Nous allons ensuite cliquer avec le bouton droit sur le dossier et choisir la commande de menu Ajouter-Nouveau> test . La boîte de dialogue « Ajouter un nouveau test » s’affiche.

Nous allons choisir de créer un « test unitaire » et de le nommer « DinnerTest.cs » :

Capture d’écran de la boîte de dialogue Ajouter un nouveau test. Test unitaire est mis en surbrillance. Dinner Test dot c s est écrit en tant que nom de test.

Quand nous cliquons sur le bouton « ok », Visual Studio ajoute (et ouvre) un fichier DinnerTest.cs au projet :

Capture d’écran du fichier Dinner Test dot c s dans Visual Studio.

Le modèle de test unitaire Visual Studio par défaut contient un tas de code réutilisable que je trouve un peu désordonné. Nous allons propre pour contenir simplement le code ci-dessous :

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NerdDinner.Models;

namespace NerdDinner.Tests.Models {
 
    [TestClass]
    public class DinnerTest {

    }
}

L’attribut [TestClass] de la classe DinnerTest ci-dessus l’identifie comme une classe qui contiendra des tests, ainsi que du code d’initialisation de test et de retrait facultatifs. Nous pouvons définir des tests dans celui-ci en ajoutant des méthodes publiques qui ont un attribut [TestMethod] sur elles.

Voici le premier des deux tests que nous allons ajouter que l’exercice de notre classe dîner. Le premier test vérifie que notre dîner n’est pas valide si un nouveau dinner est créé sans que toutes les propriétés soient correctement définies. Le deuxième test vérifie que notre dîner est valide lorsqu’un dîner a toutes les propriétés définies avec des valeurs valides :

[TestClass]
public class DinnerTest {

    [TestMethod]
    public void Dinner_Should_Not_Be_Valid_When_Some_Properties_Incorrect() {

        //Arrange
        Dinner dinner = new Dinner() {
            Title = "Test title",
            Country = "USA",
            ContactPhone = "BOGUS"
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsFalse(isValid);
    }

    [TestMethod]
    public void Dinner_Should_Be_Valid_When_All_Properties_Correct() {
        
        //Arrange
        Dinner dinner = new Dinner {
            Title = "Test title",
            Description = "Some description",
            EventDate = DateTime.Now,
            HostedBy = "ScottGu",
            Address = "One Microsoft Way",
            Country = "USA",
            ContactPhone = "425-703-8072",
            Latitude = 93,
            Longitude = -92,
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsTrue(isValid);
    }
}

Vous remarquerez ci-dessus que nos noms de test sont très explicites (et assez détaillés). Nous faisons cela parce que nous pouvons finir par créer des centaines ou des milliers de petits tests, et nous voulons faciliter la détermination rapide de l’intention et du comportement de chacun d’entre eux (en particulier lorsque nous examinons une liste d’échecs dans un exécuteur de test). Les noms des tests doivent être nommés d’après la fonctionnalité qu’ils testent. Ci-dessus, nous utilisons un modèle de nommage « Noun_Should_Verb ».

Nous structurerons les tests à l’aide du modèle de test « AAA », qui signifie « Arranger, Agir, Affirmer » :

  • Organiser : configurer l’unité en cours de test
  • Agir : Exercer l’unité testée et capturer les résultats
  • Assert : vérifier le comportement

Lorsque nous écrivons des tests, nous voulons éviter que les tests individuels en fassent trop. Au lieu de cela, chaque test ne doit vérifier qu’un seul concept (ce qui facilite considérablement l’identification de la cause des défaillances). Une bonne recommandation consiste à essayer et à n’avoir qu’une seule instruction d’assertion pour chaque test. Si vous avez plusieurs instructions d’assertion dans une méthode de test, assurez-vous qu’elles sont toutes utilisées pour tester le même concept. En cas de doute, faites un autre test.

Exécution des tests

Visual Studio 2008 Professional (et versions ultérieures) inclut un exécuteur de test intégré qui peut être utilisé pour exécuter des projets de test unitaire Visual Studio dans l’IDE. Nous pouvons sélectionner la commande de menu Test-Run-All>> Tests in Solution (ou taper Ctrl R, A) pour exécuter tous nos tests unitaires. Ou bien, nous pouvons placer notre curseur dans une classe de test ou une méthode de test spécifique et utiliser la commande de menu Test-Run-Tests>> in Current Context (ou tapez Ctrl R, T) pour exécuter un sous-ensemble des tests unitaires.

Nous allons positionner notre curseur dans la classe DinnerTest et taper « Ctrl R, T » pour exécuter les deux tests que nous venons de définir. Lorsque nous procédons ainsi, une fenêtre « Résultats des tests » s’affiche dans Visual Studio et nous voyons les résultats de notre série de tests répertoriés dans celle-ci :

Capture d’écran de la fenêtre Résultats des tests dans Visual Studio. Les résultats de la série de tests sont répertoriés dans .

Remarque : La fenêtre des résultats du test VS n’affiche pas la colonne Nom de la classe par défaut. Vous pouvez l’ajouter en cliquant avec le bouton droit dans la fenêtre Résultats des tests et en utilisant la commande de menu Ajouter/Supprimer des colonnes.

L’exécution de nos deux tests n’a pris qu’une fraction de seconde et, comme vous pouvez le voir, ils ont tous les deux réussi. Nous pouvons maintenant continuer à les augmenter en créant des tests supplémentaires qui vérifient des validations de règles spécifiques, ainsi qu’en couvrant les deux méthodes d’assistance - IsUserHost() et IsUserRegistered() - que nous avons ajoutées à la classe Dinner. Avoir tous ces tests en place pour la classe dîner rendra beaucoup plus facile et plus sûr d’y ajouter de nouvelles règles d’entreprise et des validations à l’avenir. Nous pouvons ajouter notre nouvelle logique de règle à Dinner, puis vérifier en quelques secondes qu’elle n’a rompu aucune de nos fonctionnalités logiques précédentes.

Notez que l’utilisation d’un nom de test descriptif permet de comprendre rapidement ce que chaque test vérifie. Je vous recommande d’utiliser la commande de menu Outils-Options>, d’ouvrir l’écran de configuration Outils de test-Test> Execution et de cocher la case « Double-clic sur un résultat de test unitaire ayant échoué ou non concluant affiche le point de défaillance dans le test ». Cela vous permet de double-cliquer sur un échec dans la fenêtre des résultats du test et de passer immédiatement à l’échec d’assertion.

Création de tests unitaires DinnersController

Nous allons maintenant créer des tests unitaires qui vérifient nos fonctionnalités DinnersController. Nous allons commencer par cliquer avec le bouton droit sur le dossier « Controllers » dans notre projet test, puis choisir la commande de menu Ajouter-Nouveau> test . Nous allons créer un « test unitaire » et le nommer « DinnersControllerTest.cs ».

Nous allons créer deux méthodes de test qui vérifient la méthode d’action Details() sur dinnersController. Le premier vérifie qu’un affichage est retourné lorsqu’un dîner existant est demandé. Le second vérifie qu’une vue « NotFound » est retournée lorsqu’un dîner inexistant est demandé :

[TestClass]
public class DinnersControllerTest {

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_ExistingDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(1) as ViewResult;

        // Assert
        Assert.IsNotNull(result, "Expected View");
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

        // Assert
        Assert.AreEqual("NotFound", result.ViewName);
    } 
}

Le code ci-dessus compile propre. Toutefois, lorsque nous exécutons les tests, ils échouent tous les deux :

Capture d’écran du code. Les deux tests ont échoué.

Si nous examinons les messages d’erreur, nous verrons que la raison pour laquelle les tests ont échoué est due au fait que notre classe DinnersRepository n’a pas pu se connecter à une base de données. Notre application NerdDinner utilise une chaîne de connexion à un fichier SQL Server Express local qui se trouve sous le répertoire \App_Data du projet d’application NerdDinner. Étant donné que notre projet NerdDinner.Tests compile et s’exécute dans un répertoire différent du projet d’application, l’emplacement du chemin d’accès relatif de notre chaîne de connexion est incorrect.

Nous pouvons résoudre ce problème en copiant le fichier de base de données SQL Express dans notre projet de test, puis en lui ajoutant une chaîne de connexion de test appropriée dans le App.config de notre projet de test. Les tests ci-dessus seraient ainsi débloqués et en cours d’exécution.

Toutefois, le code de test unitaire utilisant une base de données réelle présente un certain nombre de défis. Plus précisément :

  • Il ralentit considérablement le temps d’exécution des tests unitaires. Plus l’exécution des tests est longue, moins vous risquez de les exécuter fréquemment. Dans l’idéal, vous souhaitez que vos tests unitaires puissent être exécutés en quelques secondes et que vous les fassiez aussi naturellement que la compilation du projet.
  • Cela complique la logique d’installation et de nettoyage dans les tests. Vous souhaitez que chaque test unitaire soit isolé et indépendant des autres (sans effets secondaires ni dépendances). Lorsque vous travaillez sur une base de données réelle, vous devez tenir compte de l’état et la réinitialiser entre les tests.

Examinons un modèle de conception appelé « injection de dépendances » qui peut nous aider à contourner ces problèmes et à éviter d’utiliser une base de données réelle avec nos tests.

Injection de dépendances

À l’heure actuelle, DinnersController est étroitement « couplé » à la classe DinnerRepository. Le « couplage » fait référence à une situation où une classe s’appuie explicitement sur une autre classe pour fonctionner :

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/Details/5

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.FindDinner(id);

        if (dinner == null)
            return View("NotFound");

        return View(dinner);
    }

Étant donné que la classe DinnerRepository nécessite l’accès à une base de données, la dépendance étroitement couplée de la classe DinnersController à l’égard de DinnerRepository nous oblige à disposer d’une base de données pour que les méthodes d’action DinnersController soient testées.

Nous pouvons contourner ce problème en utilisant un modèle de conception appelé « injection de dépendances », qui est une approche dans laquelle les dépendances (comme les classes de référentiel qui fournissent l’accès aux données) ne sont plus créées implicitement dans les classes qui les utilisent. Au lieu de cela, les dépendances peuvent être explicitement passées à la classe qui les utilise à l’aide d’arguments de constructeur. Si les dépendances sont définies à l’aide d’interfaces, nous avons alors la possibilité de passer des implémentations de dépendances « fausses » pour les scénarios de test unitaire. Cela nous permet de créer des implémentations de dépendances spécifiques aux tests qui ne nécessitent pas réellement l’accès à une base de données.

Pour voir cela en action, nous allons implémenter l’injection de dépendances avec notre DinnersController.

Extraction d’une interface IDinnerRepository

La première étape consistera à créer une interface IDinnerRepository qui encapsule le contrat de dépôt dont nos contrôleurs ont besoin pour récupérer et mettre à jour Dinners.

Nous pouvons définir ce contrat d’interface manuellement en cliquant avec le bouton droit sur le dossier \Models, puis en choisissant la commande de menu Ajouter un> nouvel élément et en créant une interface nommée IDinnerRepository.cs.

Nous pouvons également utiliser les outils de refactorisation intégrés à Visual Studio Professional (et les éditions supérieures) pour extraire et créer automatiquement une interface pour nous à partir de notre classe DinnerRepository existante. Pour extraire cette interface à l’aide de VS, positionnez simplement le curseur dans l’éditeur de texte sur la classe DinnerRepository, puis cliquez avec le bouton droit et choisissez la commande de menu Refactoriser-extraire> l’interface :

Capture d’écran montrant l’option Extraire l’interface sélectionnée dans le sous-menu Refactoriser.

La boîte de dialogue « Extraire l’interface » s’ouvre et nous invite à entrer le nom de l’interface à créer. La valeur par défaut est IDinnerRepository et sélectionne automatiquement toutes les méthodes publiques sur la classe DinnerRepository existante à ajouter à l’interface :

Capture d’écran de la fenêtre Résultats des tests dans Visual Studio.

Lorsque nous cliquons sur le bouton « ok », Visual Studio ajoute une nouvelle interface IDinnerRepository à notre application :

public interface IDinnerRepository {

    IQueryable<Dinner> FindAllDinners();
    IQueryable<Dinner> FindByLocation(float latitude, float longitude);
    IQueryable<Dinner> FindUpcomingDinners();
    Dinner             GetDinner(int id);

    void Add(Dinner dinner);
    void Delete(Dinner dinner);
    
    void Save();
}

Et notre classe DinnerRepository existante sera mise à jour afin qu’elle implémente l’interface :

public class DinnerRepository : IDinnerRepository {
   ...
}

Mise à jour de DinnersController pour prendre en charge l’injection de constructeur

Nous allons maintenant mettre à jour la classe DinnersController pour utiliser la nouvelle interface.

Actuellement DinnersController est codé en dur de sorte que son champ « dinnerRepository » est toujours une classe DinnerRepository :

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    ...
}

Nous allons le modifier afin que le champ « dinnerRepository » soit de type IDinnerRepository au lieu de DinnerRepository. Nous allons ensuite ajouter deux constructeurs DinnersController publics. L’un des constructeurs permet de passer un IDinnerRepository en tant qu’argument. L’autre est un constructeur par défaut qui utilise notre implémentation DinnerRepository existante :

public class DinnersController : Controller {

    IDinnerRepository dinnerRepository;

    public DinnersController()
        : this(new DinnerRepository()) {
    }

    public DinnersController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
    ...
}

Étant donné que ASP.NET MVC crée par défaut des classes de contrôleur à l’aide de constructeurs par défaut, notre DinnersController au moment de l’exécution continuera d’utiliser la classe DinnerRepository pour effectuer l’accès aux données.

Nous pouvons maintenant mettre à jour nos tests unitaires, cependant, pour réussir une implémentation de dépôt de dîner « factice » à l’aide du constructeur de paramètre. Ce « faux » dépôt de dîner ne nécessite pas l’accès à une base de données réelle et utilise plutôt des exemples de données en mémoire.

Création de la classe FakeDinnerRepository

Créons une classe FakeDinnerRepository.

Nous allons commencer par créer un répertoire « Fakes » dans notre projet NerdDinner.Tests, puis y ajouter une nouvelle classe FakeDinnerRepository (cliquez avec le bouton droit sur le dossier et choisissez Ajouter-Nouvelle> classe) :

Capture d’écran de l’élément de menu Ajouter une nouvelle classe. Ajouter un nouvel élément est mis en surbrillance.

Nous allons mettre à jour le code afin que la classe FakeDinnerRepository implémente l’interface IDinnerRepository. Nous pouvons ensuite cliquer dessus avec le bouton droit et choisir la commande de menu contextuel « Implémenter l’interface IDinnerRepository » :

Capture d’écran de la commande de menu contextuel Implémenter l’interface I Dinner Repository.

Visual Studio ajoute alors automatiquement tous les membres de l’interface IDinnerRepository à notre classe FakeDinnerRepository avec les implémentations « stub out » par défaut :

public class FakeDinnerRepository : IDinnerRepository {

    public IQueryable<Dinner> FindAllDinners() {
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float long){
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        throw new NotImplementedException();
    }

    public Dinner GetDinner(int id) {
        throw new NotImplementedException();
    }

    public void Add(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Delete(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Save() {
        throw new NotImplementedException();
    }
}

Nous pouvons ensuite mettre à jour l’implémentation FakeDinnerRepository pour qu’elle fonctionne à partir d’une collection List<Dinner> en mémoire qui lui a été transmise en tant qu’argument de constructeur :

public class FakeDinnerRepository : IDinnerRepository {

    private List<Dinner> dinnerList;

    public FakeDinnerRepository(List<Dinner> dinners) {
        dinnerList = dinners;
    }

    public IQueryable<Dinner> FindAllDinners() {
        return dinnerList.AsQueryable();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        return (from dinner in dinnerList
                where dinner.EventDate > DateTime.Now
                select dinner).AsQueryable();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float lon) {
        return (from dinner in dinnerList
                where dinner.Latitude == lat && dinner.Longitude == lon
                select dinner).AsQueryable();
    }

    public Dinner GetDinner(int id) {
        return dinnerList.SingleOrDefault(d => d.DinnerID == id);
    }

    public void Add(Dinner dinner) {
        dinnerList.Add(dinner);
    }

    public void Delete(Dinner dinner) {
        dinnerList.Remove(dinner);
    }

    public void Save() {
        foreach (Dinner dinner in dinnerList) {
            if (!dinner.IsValid)
                throw new ApplicationException("Rule violations");
        }
    }
}

Nous avons maintenant une implémentation IDinnerRepository factice qui ne nécessite pas de base de données et peut à la place travailler sur une liste en mémoire d’objets Dinner.

Utilisation de FakeDinnerRepository avec des tests unitaires

Revenons aux tests unitaires DinnersController qui ont échoué précédemment, car la base de données n’était pas disponible. Nous pouvons mettre à jour les méthodes de test pour utiliser un FakeDinnerRepository rempli avec des exemples de données Dinner en mémoire sur DinnersController à l’aide du code ci-dessous :

[TestClass]
public class DinnersControllerTest {

    List<Dinner> CreateTestDinners() {

        List<Dinner> dinners = new List<Dinner>();

        for (int i = 0; i < 101; i++) {

            Dinner sampleDinner = new Dinner() {
                DinnerID = i,
                Title = "Sample Dinner",
                HostedBy = "SomeUser",
                Address = "Some Address",
                Country = "USA",
                ContactPhone = "425-555-1212",
                Description = "Some description",
                EventDate = DateTime.Now.AddDays(i),
                Latitude = 99,
                Longitude = -99
            };
            
            dinners.Add(sampleDinner);
        }
        
        return dinners;
    }

    DinnersController CreateDinnersController() {
        var repository = new FakeDinnerRepository(CreateTestDinners());
        return new DinnersController(repository);
    }

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_Dinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(1);

        // Assert
        Assert.IsInstanceOfType(result, typeof(ViewResult));
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

        // Assert
        Assert.AreEqual("NotFound", result.ViewName);
    }
}

Et maintenant, quand nous exécutons ces tests, ils réussissent tous les deux :

Capture d’écran des tests unitaires, les deux tests ont réussi.

Mieux encore, leur exécution ne prend qu’une fraction de seconde et ne nécessite aucune logique d’installation/nettoyage compliquée. Nous pouvons maintenant tester unitairement l’ensemble de notre code de méthode d’action DinnersController (y compris la liste, la pagination, les détails, la création, la mise à jour et la suppression) sans jamais avoir besoin de se connecter à une base de données réelle.

Rubrique latérale : Infrastructures d’injection de dépendances
L’injection manuelle de dépendances (comme nous l’avons fait ci-dessus) fonctionne bien, mais devient plus difficile à gérer à mesure que le nombre de dépendances et de composants dans une application augmente. Il existe plusieurs frameworks d’injection de dépendances pour .NET qui peuvent aider à fournir encore plus de flexibilité de gestion des dépendances. Ces frameworks, parfois appelés conteneurs « Inversion of Control » (IoC), fournissent des mécanismes qui permettent un niveau supplémentaire de prise en charge de la configuration pour spécifier et transmettre des dépendances aux objets au moment de l’exécution (le plus souvent à l’aide de l’injection de constructeur). Certaines des infrastructures IOC/INJECTION de dépendances OSS les plus populaires dans .NET incluent : AutoFac, Ninject, Spring.NET, StructureMap et Windsor. ASP.NET MVC expose des API d’extensibilité qui permettent aux développeurs de participer à la résolution et à l’instanciation des contrôleurs, et qui permettent aux infrastructures d’injection de dépendances/IoC d’être correctement intégrées dans ce processus. L’utilisation d’une infrastructure DI/IOC nous permettrait également de supprimer le constructeur par défaut de notre DinnersController, ce qui supprimerait complètement le couplage entre celui-ci et dinnerRepository. Nous n’utiliserons pas d’injection de dépendances/framework IOC avec notre application NerdDinner. Mais c’est quelque chose que nous pourrions envisager pour l’avenir si la base de code et les capacités de NerdDinner ont augmenté.

Création de tests unitaires d’action de modification

Nous allons maintenant créer des tests unitaires qui vérifient la fonctionnalité Modifier de DinnersController. Nous allons commencer par tester la version HTTP-GET de notre action Modifier :

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    return View(new DinnerFormViewModel(dinner));
}

Nous allons créer un test qui vérifie qu’un affichage soutenu par un objet DinnerFormViewModel est restitué lorsqu’un dîner valide est demandé :

[TestMethod]
public void EditAction_Should_Return_View_For_ValidDinner() {

    // Arrange
    var controller = CreateDinnersController();

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

Toutefois, lorsque nous exécutons le test, nous constatons qu’il échoue, car une exception de référence Null est levée lorsque la méthode Edit accède à la propriété User.Identity.Name pour effectuer l’case activée Dinner.IsHostedBy().

L’objet User de la classe de base controller encapsule des détails sur l’utilisateur connecté et est rempli par ASP.NET MVC lorsqu’il crée le contrôleur au moment de l’exécution. Étant donné que nous testons DinnersController en dehors d’un environnement de serveur web, l’objet User n’est pas défini (d’où l’exception de référence Null).

Simulation de la propriété User.Identity.Name

Les frameworks de simulation facilitent les tests en nous permettant de créer dynamiquement de fausses versions d’objets dépendants qui prennent en charge nos tests. Par exemple, nous pouvons utiliser une infrastructure de simulation dans notre test d’action Modifier pour créer dynamiquement un objet User que notre DinnersController peut utiliser pour rechercher un nom d’utilisateur simulé. Cela permet d’éviter qu’une référence null ne soit levée lorsque nous exécutons notre test.

Il existe de nombreux frameworks de simulation .NET qui peuvent être utilisés avec ASP.NET MVC (vous pouvez en voir la liste ici : http://www.mockframeworks.com/).

Une fois téléchargé, nous ajouterons une référence dans notre projet NerdDinner.Tests à l’assembly Moq.dll :

Capture d’écran de l’arborescence de navigation Nerd Dinner. Moq est mis en surbrillance.

Nous allons ensuite ajouter une méthode d’assistance « CreateDinnersControllerAs(username) » à notre classe de test qui prend un nom d’utilisateur comme paramètre, puis « se moque » de la propriété User.Identity.Name sur le instance DinnersController :

DinnersController CreateDinnersControllerAs(string userName) {

    var mock = new Mock<ControllerContext>();
    mock.SetupGet(p => p.HttpContext.User.Identity.Name).Returns(userName);
    mock.SetupGet(p => p.HttpContext.Request.IsAuthenticated).Returns(true);

    var controller = CreateDinnersController();
    controller.ControllerContext = mock.Object;

    return controller;
}

Ci-dessus, nous utilisons Moq pour créer un objet Mock qui simule un objet ControllerContext (ce que ASP.NET MVC transmet aux classes controller pour exposer des objets runtime tels que User, Request, Response et Session). Nous appelons la méthode « SetupGet » sur le Mock pour indiquer que la propriété HttpContext.User.Identity.Name sur ControllerContext doit retourner la chaîne de nom d’utilisateur que nous avons passée à la méthode d’assistance.

Nous pouvons simuler n’importe quel nombre de propriétés et de méthodes ControllerContext. Pour illustrer cela, j’ai également ajouté un appel SetupGet() pour la propriété Request.IsAuthenticated (qui n’est pas réellement nécessaire pour les tests ci-dessous, mais qui permet d’illustrer comment vous pouvez simuler des propriétés Request). Lorsque nous avons terminé, nous affectons une instance du modèle ControllerContext au DinnersController que notre méthode d’assistance retourne.

Nous pouvons maintenant écrire des tests unitaires qui utilisent cette méthode d’assistance pour tester des scénarios de modification impliquant différents utilisateurs :

[TestMethod]
public void EditAction_Should_Return_EditView_When_ValidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

[TestMethod]
public void EditAction_Should_Return_InvalidOwnerView_When_InvalidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("NotOwnerUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.AreEqual(result.ViewName, "InvalidOwner");
}

Et maintenant, quand nous exécutons les tests, ils réussissent :

Capture d’écran des tests unitaires qui utilisent la méthode d’assistance. Les tests ont réussi.

Test des scénarios UpdateModel()

Nous avons créé des tests qui couvrent la version HTTP-GET de l’action Modifier. Nous allons maintenant créer des tests qui vérifient la version HTTP-POST de l’action Modifier :

//
// POST: /Dinners/Edit/5

[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Edit (int id, FormCollection collection) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    try {
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
        ModelState.AddModelErrors(dinner.GetRuleViolations());
        
        return View(new DinnerFormViewModel(dinner));
    }
}

Le nouveau scénario de test intéressant que nous prenons en charge avec cette méthode d’action est son utilisation de la méthode d’assistance UpdateModel() sur la classe de base controller. Nous utilisons cette méthode d’assistance pour lier des valeurs form-post à notre objet Dinner instance.

Vous trouverez ci-dessous deux tests qui montrent comment nous pouvons fournir des valeurs de formulaire publiées pour la méthode d’assistance UpdateModel() à utiliser. Pour ce faire, nous allons créer et remplir un objet FormCollection, puis l’affecter à la propriété « ValueProvider » sur le contrôleur.

Le premier test vérifie que lors d’un enregistrement réussi, le navigateur est redirigé vers l’action de détails. Le deuxième test vérifie qu’en cas de publication d’une entrée non valide, l’action réaffiche la vue d’édition avec un message d’erreur.

[TestMethod]
public void EditAction_Should_Redirect_When_Update_Successful() {

    // Arrange      
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "Title", "Another value" },
        { "Description", "Another description" }
    };

    controller.ValueProvider = formValues.ToValueProvider();
    
    // Act
    var result = controller.Edit(1, formValues) as RedirectToRouteResult;

    // Assert
    Assert.AreEqual("Details", result.RouteValues["Action"]);
}

[TestMethod]
public void EditAction_Should_Redisplay_With_Errors_When_Update_Fails() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "EventDate", "Bogus date value!!!"}
    };

    controller.ValueProvider = formValues.ToValueProvider();

    // Act
    var result = controller.Edit(1, formValues) as ViewResult;

    // Assert
    Assert.IsNotNull(result, "Expected redisplay of view");
    Assert.IsTrue(result.ViewData.ModelState.Count > 0, "Expected errors");
}

Test Wrap-Up

Nous avons abordé les concepts de base impliqués dans les classes de contrôleur de test unitaire. Nous pouvons utiliser ces techniques pour créer facilement des centaines de tests simples qui vérifient le comportement de notre application.

Étant donné que nos tests de contrôleur et de modèle ne nécessitent pas de base de données réelle, ils sont extrêmement rapides et faciles à exécuter. Nous serons en mesure d’exécuter des centaines de tests automatisés en quelques secondes et d’obtenir immédiatement des commentaires sur la question de savoir si une modification que nous avons apportée a cassé quelque chose. Cela nous permettra d’améliorer, de refactoriser et d’affiner continuellement notre application.

Nous avons abordé les tests comme la dernière rubrique de ce chapitre, mais pas parce que le test est quelque chose que vous devez faire à la fin d’un processus de développement ! Au contraire, vous devez écrire des tests automatisés le plus tôt possible dans votre processus de développement. Cela vous permet d’obtenir des commentaires immédiats à mesure que vous développez, vous aide à réfléchir aux scénarios de cas d’usage de votre application et vous guide dans la conception de votre application en mettant à l’esprit propre la superposition et le couplage.

Un chapitre ultérieur du livre abordera le développement piloté par les tests (TDD) et la façon de l’utiliser avec ASP.NET MVC. Le TDD est une pratique de codage itérative dans laquelle vous écrivez d’abord les tests que votre code obtenu satisfait. Avec TDD, vous commencez chaque fonctionnalité en créant un test qui vérifie la fonctionnalité que vous êtes sur le point d’implémenter. Écrire d’abord le test unitaire vous permet de vous assurer que vous comprenez clairement la fonctionnalité et comment elle est censée fonctionner. Ce n’est qu’après l’écriture du test (et que vous avez vérifié qu’il échoue) que vous implémentez la fonctionnalité réelle qu’il vérifie. Étant donné que vous avez déjà passé du temps à réfléchir au cas d’usage de la façon dont la fonctionnalité est censée fonctionner, vous aurez une meilleure compréhension des exigences et de la meilleure façon de les implémenter. Lorsque vous avez terminé l’implémentation, vous pouvez réexécuter le test et obtenir des commentaires immédiats pour savoir si la fonctionnalité fonctionne correctement. Nous aborderons plus en détail tdd dans le chapitre 10.

étape suivante

Quelques derniers commentaires de conclusion.