• 30 heures
  • Moyenne

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 23/11/2017

Les tests automatiques

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

En général, tester une application est plutôt rébarbatif et compliqué. C'est une pratique souvent mise de côté.

Un des gros avantages de MVC est qu’avec ce découpage, il devient très facile de tester unitairement le modèle et les contrôleurs (par contre, les vues c’est toujours compliqué !).

Nous allons parler des tests dans ce chapitre, suffisamment rapidement pour ne pas vous ennuyer rassurez-vous. Mais il est utile de commencer à voir de quoi il s’agit car ils vont nous servir dans les chapitres qui suivent, lorsque nous voudrons tester notre modèle et nos contrôleurs.

Qu’est-ce qu’un test unitaire et que tester ?

Un test constitue une façon de vérifier qu’un système informatique fonctionne. Tester son application est une bonne pratique. Il faut absolument le faire.

Il y a plusieurs façons de faire des tests. Celle qui semble la plus naturelle est celle qui se fait manuellement. On lance son application web, on clique partout, on regarde si elle fonctionne. Cela veut dire que le fait de tester la partie visuelle de notre application web implique également de tester en même temps les règles métier de l'application.

La façon de faire des tests que je vais présenter ici est un complément et va nous permettre de vérifier que des bouts de code fonctionnent, de manière simple et automatisée. Avec ces tests automatisés, nous allons pouvoir tester le modèle et le contrôleur.

Nos tests automatisés doivent si possible couvrir tous les scénarios de notre application. Lorsque les tests couvrent tous les scénarios d’un code, nous pouvons assurer que notre code fonctionne. De plus, cela permet de faire des opérations de maintenance sur le code tout en étant certain qu’il n’aura pas subi de régressions. De la même façon, les tests sont un filet de sécurité lorsqu’on souhaite refactoriser son code ou l’optimiser.

Un test est donc un bout de code qui permet de tester un autre code.

Pour plus d’informations sur les tests, vous pouvez consulter ce chapitre dans le cours pour apprendre le C#.

Créer un projet de test

Ce rappel fait, rentrons tout de suite au cœur des tests.
Il y a deux solutions pour créer un projet de test. La première est de le faire au moment de la création d’un projet (rappelez-vous il y a une case à cocher nous permettant de créer un projet de test). Créons donc un nouveau projet ASP.NET MVC 4, que nous appelons par exemple DemoTests. Choisissez toujours le modèle "Empty" mais cette fois-ci, cochez la case permettant de créer un projet de test unitaire :

Ajouter un projet de tests à la création d'un projet ASP.NET MVC
Ajouter un projet de tests à la création d'un projet ASP.NET MVC

Vous pouvez donner un autre nom à votre projet de test, mais c’est une bonne convention de le suffixer par Tests, comme ce qui est proposé. Validez la création de la solution et vous pouvez constater que celle-ci contient maintenant deux projets : notre projet ASP.NET MVC connu et notre nouveau projet de tests automatiques.

Le projet de tests dans la solution
Le projet de tests dans la solution

La seconde solution pour ajouter un projet de test à une application existante est de passer par l’ajout d’un projet de test. Il suffit de faire un clic droit sur la solution, de choisir d’ajouter un nouveau projet, et d’utiliser le modèle de projet de test unitaire :

Ajouter un projet de tests
Ajouter un projet de tests

Le résultat sera le même. Dans ce dernier cas, il vous faudra cependant ajouter une référence à votre projet web depuis votre projet de test.

Un premier test

Nous allons à présent créer notre premier test. Ce sera un test exemple pas du tout en rapport avec une application web MVC, mais ne vous inquiétez pas, cela changera vite. :p

Nous pouvons voir que Visual Studio nous a créé un fichier de test par défaut, dont le nommage UnitTest1.cs laisse à désirer. Dans notre exemple, ce n'est pas trop grave, mais si cela vous démange, n’hésitez pas à renommer la classe avec un nom opportun. Il est important d’avoir des noms de classe de tests et des noms de méthodes de tests qui suivent une norme que tout le monde suivra afin de s’y retrouver.
Ici, pour notre exemple je vais tester des méthodes de math basiques, donc je vais renommer ma classe en MathTests .

Vous remarquerez que le contenu du fichier généré est le suivant :

[TestClass]
public class MathTests
{
    [TestMethod]
    public void TestMethod1()
    {
    }
}

L’attribut TestClass  permet d’indiquer au framework de test que la classe MathTests contient des méthodes de test et qu’il va falloir les exécuter. Sans cet attribut, le framework de test n’est pas capable d’identifier une classe contenant des méthodes de test.
De la même façon, la méthode de test est décorée de l’attribut TestMethod .

Ici aussi, il est intéressant de suivre une règle de nommage afin de pouvoir identifier rapidement l’intention de la méthode de test. Je vous propose le nommage suivant : MethodeTestee_EtatInitial_EtatAttendu() . Par exemple, une méthode permettant de tester une méthode de calcul de factorielle pourrait s’appeler Factorielle_AvecValeur3_Retourne6 . Comme il est important de tester plusieurs scénarios de tests, je vous encourage à créer beaucoup de méthodes de tests.

Il est temps de se lancer. Nous allons créer une méthode permettant de calculer une factorielle et nous allons la tester. Créez donc une classe MathHelper  dans votre projet web et mettez là où vous voulez, ce n’est pas bien grave, c’est pour l’exemple ; vous pourrez la supprimer ensuite :

public static class MathHelper
{
    public static long Factorielle(int a)
    {
        if (a <= 1)
            return 1;
        return a * Factorielle(a - 1);
    }
}

Puis modifiez votre classe de tests pour avoir les trois tests suivants, nous permettant de vérifier que la méthode de calcul de factorielle est valide :

[TestClass]
public class MathTests
{
    [TestMethod]
    public void Factorielle_AvecValeur0_Retourne1()
    {
        double resultat = MathHelper.Factorielle(0);
        Assert.AreEqual(1, resultat);
    }

    [TestMethod]
    public void Factorielle_AvecValeur3_Retourne6()
    {
        double resultat = MathHelper.Factorielle(3);
        Assert.AreEqual(6, resultat);
    }

    [TestMethod]
    public void Factorielle_AvecValeur15_Retourne1307674368000()
    {
        double resultat = MathHelper.Factorielle(15);
        Assert.AreEqual(6, resultat);
    }
}

Comme Visual Studio est sympa, il nous a rajouté la référence lui-même lorsque nous avons créé le projet de tests en même temps que le projet ASP.NET MVC. Si vous avez créé le projet de test ultérieurement à la main, il faudra bien sûr rajouter votre référence au projet web.

Les méthodes de la classe statique Assert  permettent de vérifier nos résultats. Ici, la méthode AreEqual  permet de vérifier qu’une valeur est bien égale à une autre, ce qui est la condition pour que le test fonctionne.

Bien sûr, vous aurez remarqué que ma dernière méthode va échouer, car la factorielle de 15 ne vaut évidemment pas 6 !

Il ne nous reste plus qu’à exécuter les tests et à voir si tout se passe bien… Allez dans le menu TEST et choisissez d’exécuter tous les tests :

Exécuter les tests
Exécuter les tests

Une nouvelle fenêtre s’ouvre nous montrant le résultat de nos tests :

Résultats des tests
Résultats des tests

Deux tests réussis et un test raté, ce qui était prévu… De plus, si nous sélectionnons le test raté, nous pouvons avoir plus d’infos sur l’échec. Corrigeons donc le dernier test pour qu’il passe :

[TestMethod]
public void Factorielle_AvecValeur10_Retourne1307674368000()
{
    double resultat = MathHelper.Factorielle(15);
    Assert.AreEqual(1307674368000, resultat);
}

Et voilà, tous les tests sont verts. :) Chouette.

Le framework de simulacre

Un framework de simulacre fournit un moyen de tester une méthode en l’isolant du reste du système. Imaginons par exemple une méthode qui permette de récupérer la météo du jour, en allant la lire à partir d'un service web. Nous avons ici un problème car lorsque nous exécutons le test le lundi, il pleut. Quand nous exécutons le test le mardi, il fait beau, etc. Nous avons ici une information qui varie au cours du temps. Il est donc difficile de tester automatiquement que la méthode arrive bien à construire la météo du jour à partir de ces informations, vu qu’elles varient.

Le but des frameworks de simulacre est de pouvoir bouchonner le code dont notre développement dépend afin de pouvoir le tester unitairement, sans dépendance et isolé du reste du système.

Cela veut dire que dans notre test, nous allons remplacer l'interrogation du service web par une fausse méthode qui renvoie toujours qu’il fait beau. Cependant, ceci doit se faire sans modifier notre application, sinon cela n’a pas d’intérêt. Voilà à quoi servent ces framework de simulacres.

Il en existe plusieurs, plus ou moins complexes. Citons par exemple Moq (prononcez « moque-you ») qui fonctionne très bien avec ASP.NET MVC et qui est gratuit, ou encore le nouveau framework Fakes de Microsoft qui vient avec la version la plus chère de Visual Studio, mais il y en a plein d’autres... L’intérêt de Moq est qu’il est simple d’accès : il permet de faire des choses simples et facilement. Fakes est un peu plus évolué mais plus complexe à prendre en main. Vous y reviendrez peut-être ultérieurement. Pour le moment, nous allons utiliser Moq dans ce cours et le plus simple pour s'en servir est d'utiliser NuGet pour l'installer. Faites un clic droit sur le projet de tests et choisissez de gérér les packages NuGet :

Gérer les packages NuGet
Gérer les packages NuGet

Cherchez parmi les packages en ligne Moq et choisissez de l'installer :

Installer Moq via NuGet
Installer Moq via NuGet

Une fois l'installation terminée, vous pouvez fermer la fenêtre. Vous pouvez constater que l'assembly Moq a été référencée automatiquement dans le projet de tests.

Ensuite, pour pouvoir bouchonner facilement une classe, celle-ci doit implémenter une interface.
Imaginons la classe d’accès aux données suivante :

public class Dal : IDal
{
    public Meteo ObtenirLaMeteoDuJour()
    {
        // ici , c'est le code pour accéder au service web
        // mais finalement , peu importe ce qu'on y met vu qu'on va bouchonner la méthode
        throw new NotImplementedException();
    }
}

qui implémente l’interface suivante :

public interface IDal
{
    Meteo ObtenirLaMeteoDuJour();
}

avec l’objet Meteo  suivant :

public class Meteo
{
    public double Temperature { get; set; }
    public Temps Temps { get; set; }
}

Et l’énumération Temps  suivante :

public enum Temps
{
    Soleil,
    Pluie
}

Nous pourrons écrire un test qui bouchonne l’appel à la méthode ObtenirLaMeteoDuJour , qui doit normalement aller interroger un service web, pour renvoyer un faux objet à la place.
Pour bien montrer ce fonctionnement, j’ai fait en sorte que la méthode lève une exception, comme ça, si on passe dedans, ça sera tout de suite visible. La méthode de test classique devrait être :

[TestMethod]
public void ObtenirLaMeteoDuJour_AvecUnBouchon_RetourneSoleil()
{
    IDal dal = new Dal();
    Meteo meteoDuJour = dal.ObtenirLaMeteoDuJour();
    Assert.AreEqual(25, meteoDuJour.Temperature);
    Assert.AreEqual(Temps.Soleil, meteoDuJour.Temps);
}

Si nous exécutons le test, nous aurons bien sûr une exception car la méthode n’est pas implémentée. Normal. :)
Utilisons maintenant Moq pour bouchonner cet appel et le remplacer par ce que l’on veut :

[TestMethod]
public void ObtenirLaMeteoDuJour_AvecUnBouchon_RetourneSoleil()
{
    Meteo fausseMeteo = new Meteo
    {
        Temperature = 25,
        Temps = Temps.Soleil
    };
    Mock<IDal> mock = new Mock<IDal>();
    mock.Setup(dal => dal.ObtenirLaMeteoDuJour()).Returns(fausseMeteo);

    IDal fausseDal = mock.Object;
    Meteo meteoDuJour = fausseDal.ObtenirLaMeteoDuJour();
    Assert.AreEqual(25, meteoDuJour.Temperature);
    Assert.AreEqual(Temps.Soleil, meteoDuJour.Temps);
}

On utilise l’objet générique Mock pour créer un faux objet du type de notre interface. On utilise la méthode Setup  à travers une expression lambda pour indiquer que la méthode ObtenirLaMeteoDuJour  retournera en fait un faux objet météo. Cela se fait tout naturellement en utilisant la méthode Returns() .

On obtient ensuite une instance de notre objet grâce à la propriété Object  et c’est ce faux objet que nous pourrons comparer à nos valeurs.

Bien sûr, ici, ce test n’a pas grand intérêt, mais imaginons que nous ayons besoin de tester la fonctionnalité qui met en forme cet objet météo récupéré du service web ou bien l’algorithme qui nous permet de faire des statistiques sur ces données météos. . . Là, nous sommes sûrs de pouvoir nous baser sur une valeur connue de la dépendance au service web (et en plus, nous ne sommes pas obligés d'être connectés à Internet). Cela permettra également de décliner tous les cas envisageables en changeant la valeur du bouchon et de faire les tests les plus exhaustifs possibles.
Nous pouvons faire la même chose avec les propriétés. Imaginons la classe suivante dont la propriété Valeur  retourne un nombre aléatoire :

public interface IGenerateur
{
    int Valeur { get; }
}

public class Generateur : IGenerateur
{
    private Random r;
    public Generateur()
    {
        r = new Random();
    }

    public int Valeur
    {
        get
        {
            return r.Next(0, 100);
        }
    }
}

Nous pourrions avoir besoin de bouchonner cette propriété pour qu’elle renvoie un nombre connu à l’avance. Cela se fera de la même façon :

Mock<IGenerateur> mock = new Mock<IGenerateur>();
mock.Setup(generateur => generateur.Valeur).Returns(5);

Assert.AreEqual(5, mock.Object.Valeur);

Ici, la propriété Valeur  renverra toujours 5 en se moquant ( :p ) bien du générateur de nombre aléatoire. . .

Je m’arrête là pour l’aperçu de ce framework de simulacre. Nous avons pu voir qu’il pouvait facilement bouchonner des dépendances nous permettant de faciliter la mise en place de nos tests unitaires. Rappelez-vous, pour qu’un test soit efficace, il doit pouvoir se concentrer sur un point précis du code sans être embêté par les dépendances éventuelles qui peuvent perturber l’état du test à un instant t.

Une première application : tester les routes

Bon, c’est bien beau ce framework de simulacre. Nous avons compris globalement l’intérêt, mais à quoi va-t-il vraiment nous servir avec ASP.NET MVC ?

À plein de choses, mais dans un premier temps, il va nous permettre de tester nos routes et plus particulièrement de vérifier si les différentes URL que nous souhaitons utiliser déclenchent bien les bonnes instanciations de contrôleurs avec les bons paramètres… Parce que nous avons vu qu’avec plusieurs routes, cela pouvait vite être complexe. Et si nous avons besoin d’en rajouter une ? Comment être surs que toutes les autres règles ne vont pas être déréglées ?

Les tests bien sûr !

Pour cet exemple, reprenons notre solution HelloWorld et positionnez les deux routes que nous avons vues dans le chapitre précédent :

public class RouteConfig
{
    public static void RegisterRoutes(RouteCollection routes)
    {
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

        routes.MapRoute(
            name: "Meteo",
            url: "{jour}/{mois}/{annee}",
            defaults: new { controller = "Meteo", action = "Afficher" },
            constraints: new { jour = @"\d+", mois = @"\d+", annee = @"\d+" });

        routes.MapRoute(
            name: "Default",
            url: "{controller}/{action}/{id}",
            defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
        );
    }
}

Nous retrouvons notre route pouvant aiguiller des URL de la sorte /28/04/2013 vers la méthode Afficher  du contrôleur Meteo, avec comme paramètres le jour 28, le mois 04 et l’année 2013.
De même, une URL du genre /Home/Index/2 permettra d’exécuter la méthode Index  du contrôleur Home  avec en paramètre un id  à 2. Rappelez-vous également que la route / instanciera le contrôleur Home  et appellera la méthode Index , qui sont les valeurs par défaut.

Enfin… ça, c’est la théorie, et si nous le vérifiions avec des tests ? ;)

Pour tester cet élément, jusqu'à maintenant nous ne pouvions le faire qu’en démarrant une application web et en vérifiant par nous-même que l’URL instanciait bien le contrôleur, car le routing utilise les rouages internes d’ASP.NET. Sauf qu’avec Moq, nous allons pouvoir nous en passer.

Au final, ce que nous voulons vérifier, c’est que la méthode qui déclare nos routes fait bien son travail. La méthode concernée est la méthode statique publique RegisterRoutes , faisant partie de la classe statique publique RouteConfig . Cette construction statique est en fait très pratique, nous allons pouvoir l’appeler depuis notre test en lui passant une RouteCollection  de notre cru, ce qui nous permettra de vérifier que tout correspond bien. Nous pourrons alors inspecter le contenu de la route choisie par ASP.NET MVC.

Pour cela, il faut un peu comprendre comment fonctionne le système de routing en interne. En fait, la méthode MapRoute  que l’on utilise pour déclarer une nouvelle route appelle une méthode du framework MVC qui crée un objet RouteBase  servant à mapper une requête web et la route. Elle utilise la classe HttpContextBase  qui contient des informations sur une requête HTTP. Nous allons créer un faux objet HttpContextBase  afin de définir l’URL que nous voulons tester :

Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
mockContext.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath).Returns("~/Home/Index/2");

Nous bouchonnons la propriété AppRelativeCurrentExecutionFilePath  car c’est cette propriété qui est utilisée par le routing.

Nous pourrons alors instancier un objet RouteCollection  et le passer en paramètres de notre méthode de configuration :

RouteCollection routes = new RouteCollection();
RouteConfig.RegisterRoutes(routes);
RouteData routeData = routes.GetRouteData(mockContext.Object);

Puis nous appellerons la méthode GetRouteData  pour effectuer le mapping à partir de l’URL contenue dans l’objet de type HttpContextBase .
Et enfin, nous n’aurons plus qu’à vérifier que tout est correct, en allant inspecter l’objet routeData. Ce qui donne au final le test suivant :

[TestMethod]
public void Routes_PageHomeIndex2_RetourneControleurHomeEtMethodeIndexEtParam2()
{
    Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
    mockContext.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath).Returns("~/Home/Index/2");
    RouteCollection routes = new RouteCollection();
    RouteConfig.RegisterRoutes(routes);
    RouteData routeData = routes.GetRouteData(mockContext.Object);
    Assert.IsNotNull(routeData);
    Assert.AreEqual("Home", routeData.Values["controller"]);
    Assert.AreEqual("Index", routeData.Values["action"]);
    Assert.AreEqual("2", routeData.Values["id"]);
}

L'initialisation du contexte peut sembler complexe, mais c’est toujours la même chose. Comme nous allons écrire plusieurs tests, nous pouvons refactoriser un peu l’initialisation de la route à partir de l’URL :

private static RouteData DefinirUrl(string url)
{
    Mock<HttpContextBase> mockContext = new Mock<HttpContextBase>();
    mockContext.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath).Returns(url);
    RouteCollection routes = new RouteCollection();
    RouteConfig.RegisterRoutes(routes);
    RouteData routeData = routes.GetRouteData(mockContext.Object);
    return routeData;
}

Et voyez avec les trois tests suivants comment vérifier le bon comportement des URL du genre :

  • /Home/Index/2

  • /

  • /28/04/2013

[TestMethod]
public void Routes_PageHome_RetourneControleurHomeEtMethodeIndex()
{
    RouteData routeData = DefinirUrl("~/");
    Assert.IsNotNull(routeData);
    Assert.AreEqual("Home", routeData.Values["controller"]);
    Assert.AreEqual("Index", routeData.Values["action"]);
    Assert.AreEqual(UrlParameter.Optional, routeData.Values["id"]);
}

[TestMethod]
public void Routes_PageHomeIndex2_RetourneControleurHomeEtMethodeIndexEtParam2()
{
    RouteData routeData = DefinirUrl("~/Home/Index/2");
    Assert.IsNotNull(routeData);
    Assert.AreEqual("Home", routeData.Values["controller"]);
    Assert.AreEqual("Index", routeData.Values["action"]);
    Assert.AreEqual("2", routeData.Values["id"]);
}

[TestMethod]
public void Routes_MeteoAujourdhui_RetourneControleurMeteoMethodeAfficherEtParametreAujourdhui()
{
    DateTime aujourdhui = DateTime.Now;
    RouteData routeData = DefinirUrl(string.Format("~/{0}/{1}/{2}", aujourdhui.Day, aujourdhui.Month, aujourdhui.Year));
    Assert.IsNotNull(routeData);
    Assert.AreEqual("Meteo", routeData.Values["controller"]);
    Assert.AreEqual("Afficher", routeData.Values["action"]);
    Assert.AreEqual(aujourdhui.Day.ToString(), routeData.Values["jour"]);
    Assert.AreEqual(aujourdhui.Month.ToString(), routeData.Values["mois"]);
    Assert.AreEqual(aujourdhui.Year.ToString(), routeData.Values["annee"]);
}

Et voilà pour le test des routes, plutôt pas mal non ?
Remarquez que lorsqu’une route ne correspond pas, prenez au hasard l’URL /Url/bidon/pas/bonne, alors routeData  vaudra null . Hop, un petit test pour s’en assurer :

[TestMethod]
public void Routes_UrlBidon_RetourneNull()
{
    RouteData routeData = DefinirUrl("/Url/bidon/pas/bonne");
    Assert.IsNull(routeData);
}

C’en est fini de ce petit tour des tests unitaires. Nous y reviendrons dans les chapitres ultérieurs lorsqu’il s’agira de tester d’autres composants. :)

En résumé

  • Les tests sont un élément indispensable de toute application informatique afin de vérifier qu’elle fonctionne.

  • L’architecture MVC propose un découpage qui facilite les tests du modèle et du contrôleur.

  • On peut associer nos tests automatiques à un framework de simulacre comme Moq, afin de bouchonner nos dépendances.

Exemple de certificat de réussite
Exemple de certificat de réussite