ASP.NET Core : mise en œuvre des tests unitaires dans une application MVC

Les tests unitaires sont utilisés par le programmeur pour tester indépendamment des unités de traitement (méthodes) et s'assurer de leur bon fonctionnement. Les tests unitaires offrent plus de robustesse au code et permettent de faire des opérations de maintenance sur le code sans que celui-ci ne subisse de régression.

Commentez Donner une note à l'article (5)

Article lu   fois.

L'auteur

Profil ProSite personnel

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Introduction

ASP.NET Core apporte une refonte complète de la solution de développement Web de Microsoft. Le recours à un nouvel environnement d'exécution (DNX) et son architecture entraînent une incompatibilité avec de nombreux outils.

En effet, si vous développez un projet ASP.NET Core qui utilise uniquement le CoreCLR, vous ne devez qu'utiliser les dépendances qui offrent une prise en charge du CoreCLR.

Depuis l'annonce de .NET Core, de nombreux éditeurs de librairies se sont activés pour offrir une prise en charge de cette version, c'est notamment le cas des outils de tests dont MSTest et Moq.

Microsoft a publié une préversion de la V2 de MsTest, qui introduit une architecture unifiée et apporte une prise en charge de .NET Core. Dans le cadre de cet article, nous verrons comment mettre en place des tests unitaires dans un projet ASP.NET MVC Core.

Cet article est divisé en trois parties. La première partie présentera comment mettre en œuvre un projet de tests unitaires avec MsTest V2, la seconde partie portera sur les tests unitaires mockés et la troisième et dernière partie permettra de découvrir comment exploiter la fonctionnalité InMemory d'Entity Framework Core pour effectuer des tests d'intégration.

II. Prérequis

Des connaissances de base en développement .NET sont nécessaires pour la bonne compréhension de cet article.

III. Outils de développement

Pour la rédaction de cet article, j'ai utilisé :

  • Visual Studio 2015 Update 3 ;
  • ASP.NET Core 1.0 ;
  • la préversion de MsTest V2 ;
  • la version Alpha de Moq 4.6.38 ;
  • Entity Framework Core 1.0.

IV. Code source

Vous pouvez télécharger le code source du projet d'exemple sur ma page GitHubma page GitHub.

V. Partie 1 - Tests unitaires d'une application MVC

V-A. Création du projet de test

Pour commencer, vous allez créer une application ASP.NET Core en utilisant le modèle Web Application.

Image non disponible

Cela fait, vous allez ajouter un nouveau projet de type bibliothèque de classes à votre solution. Pour l'instant, il n'existe pas de modèle de projet pour la nouvelle version du framework de test de Microsoft. Cette prise en charge sera effective dans la version stable de l'outil, avec la sortie de Visual Studio 15.

Image non disponible

Ajoutez les dépendances suivantes à votre projet en utilisant la console Nuget :

  • MSTest.TestFramework ;
  • MSTest.TestAdapter ;
  • dotnet-test-mstest.

Le package MSTest.TestFramework permet d'installer le framework MSTest V2. Pour l'installer, cliquez sur Tools dans la barre de menu de Visual Studio, puis Nuget Package Manager, ensuite sur Package Manager Console. Assurez-vous que dans la zone Default Project, vous avez sélectionné votre projet de test :

Image non disponible

Dans la console NuGet, tapez la commande suivante :

 
Sélectionnez
Install-Package MSTest.TestFramework -Pre

MSTest.TestAdapter est utilisé pour trouver et exécuter le framework de test sur lequel votre projet de test est basé. Pour l'installer, vous devez exécuter la commande suivante dans la console NuGet :

 
Sélectionnez
Install-Package MSTest.TestAdapter -Pre

Enfin, vous allez exécuter la commande suivante pour installer le dernier package NuGet :

 
Sélectionnez
Install-Package dotnet-test-mstest -Pre

Vous allez probablement obtenir un message d'erreur d'incompatibilité lors de la restauration des packages par NuGet. Ouvrez le fichier Project.json et remplacez :

 
Sélectionnez
"frameworks": {
   "netstandard1.6": {
     "imports": "dnxcore50"
   }
}

par :

 
Sélectionnez
"frameworks": {
   "netcoreapp1.0": {
     "imports": [
       "dnxcore50",
       "portable-net45+win8"
     ],
 
     "dependencies": {
       "Microsoft.NETCore.App": {
         "version": "1.0.0",
         "type": "platform"
       }
     }
   }
 }

Vous remarquerez sans doute que nous avons marqué notre bibliothèque de classes comme une application (netcoreapp1.0). Cela est dû au fait que notre projet de test utilise le .NET CLI. Sa méthode Main sera fournie par le runner de mstest.

Vous devez par la suite ajouter au fichier Project.json la ligne suivante, pour spécifier que votre projet de test unitaire repose sur mstest :

 
Sélectionnez
"testRunner": "mstest",

Ajoutez une référence au projet ASP.NET Core. Votre fichier Project.json devrait ressembler à ceci :

 
Sélectionnez
{
 "version": "1.0.0-*",

 "testRunner": "mstest",

 "dependencies": {
   "dotnet-test-mstest": "1.1.1-preview",
   "MSTest.TestAdapter": "1.0.3-preview",
   "MSTest.TestFramework": "1.0.1-preview",
   "NETStandard.Library": "1.6.0",
   "SampleApp": "1.0.0-*"
 },

 "frameworks": {
   "netcoreapp1.0": {
     "imports": [
       "dnxcore50",
       "portable-net45+win8"
     ],

     "dependencies": {
       "Microsoft.NETCore.App": {
         "version": "1.0.0",
         "type": "platform"
       }
     }
   }
 }

}

Ajoutez un nouveau fichier HomeControllerTest.cs à votre projet de test. Ajoutez un appel à l'espace de nom Microsoft.VisualStudio.TestTools.UnitTesting :

 
Sélectionnez
using Microsoft.VisualStudio.TestTools.UnitTesting;

V-B. Le code à tester

Jetons maintenant un coup d'œil au fichier HomeController.cs. J'ai légèrement modifié ce dernier pour qu'il puisse répondre à nos attentes.

 
Sélectionnez
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

namespace SampleApp.Controllers
{
   public class HomeController : Controller
   {
       public IActionResult Index()
       {
           return View();
       }

       public IActionResult About()
       {
           ViewData["Message"] = "Your application description page.";

           return View();
       }

       public IActionResult Contact()
       {
           ViewData["Message"] = "Your contact page.";

           return View();
       }

       public IActionResult Error()
       {
           return View("~/Views/Shared/Error.cshtml");
       }
   }
}

Nous allons écrire du code pour tester le ViewResult et une autre pour le ViewData.

V-C. Test du ViewResult

Le code que nous allons écrire va permettre de vérifier que la méthode d'action Error(), dont voici le code, retourne le bon ViewResult.

 
Sélectionnez
  public IActionResult Error()
       {
           return View("~/Views/Shared/Error.cshtml");
       }

Le code de test est le suivant :

 
Sélectionnez
[TestMethod]
       public void Error_ReturnsErrorView()
       {
           // Arrange
           var controller = new HomeController();
           var errorView = "~/Views/Shared/Error.cshtml";

           // Act
           var viewResult = controller.Error() as ViewResult;

           // Assert
           Assert.AreEqual(errorView, viewResult.ViewName);
       }

V-D. Test du ViewData

Pour le ViewData, nous allons tester que notre méthode d'action renvoie le bon ViewData. Voici la méthode à tester :

 
Sélectionnez
public IActionResult About()
       {
           ViewData["Message"] = "Your application description page.";

           return View();
       }

La méthode de test pour effectuer cela est la suivante :

 
Sélectionnez
[TestMethod]
       public void About_ReturnViewData()
       {
           // Arrange
           var controller = new HomeController();
           var viewData = "Your application description page.";

           // Act
           var viewResult = controller.About() as ViewResult;

           // Assert
           Assert.AreEqual(viewData, viewResult.ViewData["Message"]);
       }

Le code complet est le suivant :

 
Sélectionnez
[TestClass]
    public class HomeControllerTest
    {
        [TestMethod]
        public void Error_ReturnErrorView()
        {
            // Arrange
            var controller = new HomeController();
            var errorView = "~/Views/Shared/Error.cshtml";

            // Act
            var viewResult = controller.Error() as ViewResult;

            // Assert
            Assert.AreEqual(errorView, viewResult.ViewName);
        }


        [TestMethod]
        public void About_ReturnViewData()
        {
            // Arrange
            var controller = new HomeController();
            var viewData = "Your application description page.";

            // Act
            var viewResult = controller.About() as ViewResult;

            // Assert
            Assert.AreEqual(viewData, viewResult.ViewData["Message"]);
        }
    }

Á l'exécution, vous allez obtenir le résultat suivant dans l'explorateur de test :

Image non disponible

VI. Partie 2 : tests unitaires mockés

VI-A. Rappel sur le mocking

Lors du développement, il arrive fréquemment que dans une classe, nous fassions appel à plusieurs autres objets. Ce qui crée une dépendance entre les classes. Les tests unitaires ont pour objectifs de tester une unité de traitement (une méthode), sans avoir besoin de se soucier des dépendances avec d'autres classes (des objets qui sont appelés, et qui seront testés séparément).

Le but du mocking est de permettre aux développeurs de créer des objets simulés qui reproduisent le comportement désiré des objets réels à leur invocation. Ces objets simulés sont couramment appelés Mock.

Il existe de nombreux frameworks .NET qui permettent de mettre en œuvre facilement le mocking. Ces frameworks permettent généralement de créer dynamiquement des objets à partir d'interfaces ou de classes. Ils offrent au développeur la possibilité de spécifier quelles méthodes vont être appelées et dans quel ordre elles le seront.

Dans le cadre de ce tutoriel, nous utiliserons le framework Moq qui est une référence dans l'univers .NET. Ce dernier offre une prise en charge de .NET core.

VI-B. Description de l'application à tester

Pour la suite du tutoriel, nous allons utiliser Entity Framework. Je ne vais pas m'attarder sur l'intégration d'Entity Framewok au projet ASP.NET Core. Pour le faire, je vous invite à consulter ce billet de blogce billet de blog qui vous guidera dans ce processus.

Nous allons utiliser une base de données LocalDB avec une seule table. Notre application va permettre d'effectuer des opérations CRUD sur la table suivante :

Image non disponible

Le code que nous allons tester utilise le pattern Repository et tire avantage des améliorations qui ont été apportées à ASP.NET Core pour offrir une meilleure prise en charge de l'injection des dépendances. Avec cette version, nous n'avons plus besoin, par exemple, de mettre en œuvre l'injection des dépendances au niveau du contrôleur. Vous verrez combien cela va faciliter l'écriture de nos tests unitaires mockés.

Pour mettre en œuvre ce pattern, vous devez ajouter à votre projet un nouveau dossier Repository, ensuite y ajouter l'interface suivante :

 
Sélectionnez
 public interface IStudentsRepository
    {
        void Add(Student student);
        Task<IEnumerable<Student>> GetAll();
        Task<Student> Find(int id);
        Task Remove(int id);
        void Update(Student student);
        Task Save();
        Task<bool> StudentExists(int id);
    }

La classe qui hérite de cette interface contient le code pour effectuer des opérations CRUD sur la table Student :

 
Sélectionnez
    public class StudentsRepository : IStudentsRepository, IDisposable
    {

        private SampleAppContext _context;

        public StudentsRepository(SampleAppContext context)
        {
            _context = context;
        }

        public void Add(Student student)
        {
            _context.Add(student);
           
        }

        public async Task<Student> Find(int id)
        {
            return await _context.Students.SingleOrDefaultAsync(m => m.Id == id);

        }

        public async Task<IEnumerable<Student>> GetAll()
        {
          return  await _context.Students.ToListAsync();
        }

        public async Task Remove(int id)
        {
            var student = await _context.Students.SingleOrDefaultAsync(m => m.Id == id);
            _context.Students.Remove(student);
         
        }

        public void Update(Student student)
        {
            _context.Update(student);
            
        }

        public async Task Save()
        {

            await _context.SaveChangesAsync();
        }

        public async Task<bool> StudentExists(int id)
        {
            return await _context.Students.AnyAsync(e => e.Id == id);
        }

     
        private bool disposedValue = false; // To detect redundant calls

        protected virtual void Dispose(bool disposing)
        {
            if (!disposedValue)
            {
                if (disposing)
                {
                    _context.Dispose();
                }

                disposedValue = true;
            }
        }

       

        // This code added to correctly implement the disposable pattern.
        public void Dispose()
        {
            // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
            Dispose(true);
            // TODO: uncomment the following line if the finalizer is overridden above.
            GC.SuppressFinalize(this);
        }
       
    }

Vous devez par la suite ajouter votre interface et la classe au conteneur d'injection de dépendances de ASP.NET Core. Pour cela, vous devez éditer le fichier Startup.cs et ajouter la ligne de code suivante dans la méthode ConfigureServices :

 
Sélectionnez
services.AddScoped<IStudentsRepository, StudentsRepository>();

Nous allons maintenant créer un contrôleur ayant une référence à cette classe, avec des actions pour effectuer des opérations CRUD. Vous devez donc ajouter un fichier StudentsController.cs à votre projet, avec le code suivant :

 
Sélectionnez
 public class StudentsController : Controller
   {
       private readonly IStudentsRepository _studentsRepository;

       public StudentsController(IStudentsRepository studentsRepository)
       {
           _studentsRepository = studentsRepository;    
       }

       // GET: Students
       public async Task<IActionResult> Index()
       {
           return View(await _studentsRepository.GetAll());
       }

       // GET: Students/Details/5
       public async Task<IActionResult> Details(int? id)
       {
           if (id == null)
           {
               return NotFound();
           }

           var student = await _studentsRepository.Find(id.Value);
           if (student == null)
           {
               return NotFound();
           }

           return View(student);
       }

       // GET: Students/Create
       public IActionResult Create()
       {
           return View();
       }

       // POST: Students/Create
       // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
       // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
       [HttpPost]
       [ValidateAntiForgeryToken]
       public async Task<IActionResult> Create([Bind("Id,Email,FirstName,LastName")] Student student)
       {
           if (ModelState.IsValid)
           {
              _studentsRepository.Add(student);
 await _studentsRepository.Save();
               return RedirectToAction("Index");
           }
           return View(student);
       }

       // GET: Students/Edit/5
       public async Task<IActionResult> Edit(int? id)
       {
           if (id == null)
           {
               return NotFound();
           }

           var student = await _studentsRepository.Find(id.Value);
           if (student == null)
           {
               return NotFound();
           }
           return View(student);
       }

       // POST: Students/Edit/5
       // To protect from overposting attacks, please enable the specific properties you want to bind to, for 
       // more details see http://go.microsoft.com/fwlink/?LinkId=317598.
       [HttpPost]
       [ValidateAntiForgeryToken]
       public async Task<IActionResult> Edit(int id, [Bind("Id,Email,FirstName,LastName")] Student student)
       {
           if (id != student.Id)
           {
               return NotFound();
           }

           if (ModelState.IsValid)
           {
               try
               {
                    _studentsRepository.Update(student);
                   await _studentsRepository.Save();
               }
               catch (DbUpdateConcurrencyException)
               {
                   if (!await _studentsRepository.StudentExists(student.Id))
                   {
                       return NotFound();
                   }
                   else
                   {
                       throw;
                   }
               }
               return RedirectToAction("Index");
           }
           return View(student);
       }

       // GET: Students/Delete/5
       public async Task<IActionResult> Delete(int? id)
       {
           if (id == null)
           {
               return NotFound();
           }

           var student = await _studentsRepository.Find(id.Value);
           if (student == null)
           {
               return NotFound();
           }

           return View(student);
       }

       // POST: Students/Delete/5
       [HttpPost, ActionName("Delete")]
       [ValidateAntiForgeryToken]
       public async Task<IActionResult> DeleteConfirmed(int id)
       {
            await _studentsRepository.Remove(id);
          await _studentsRepository.Save();

           return RedirectToAction("Index");
       }

       
   }

Les méthodes de tests unitaires que nous écrirons permettront de tester les actions de ce contrôleur.

VI-C. Installation du package Moq

Revenons à notre projet de test. La première chose à faire sera d'installer le package Moq dans ce dernier en utilisant la console NuGet. La commande à utiliser est la suivante :

 
Sélectionnez
Install-Package Moq -Pre

Lorsque c'est fait, votre fichier project.json devrait ressembler à ceci :

 
Sélectionnez
{
 "version": "1.0.0-*",

 "testRunner": "mstest",

 "dependencies": {
   "dotnet-test-mstest": "1.1.1-preview",
   "Moq": "4.6.38-alpha",
   "MSTest.TestAdapter": "1.0.3-preview",
   "MSTest.TestFramework": "1.0.1-preview",
   "NETStandard.Library": "1.6.0",
   "SampleApp": "1.0.0-*"
 },

 "frameworks": {
   "netcoreapp1.0": {
     "imports": [
       "dnxcore50",
       "portable-net45+win8"
     ],

     "dependencies": {
       "Microsoft.NETCore.App": {
         "version": "1.0.0",
         "type": "platform"
       }
     }
   }
 }

}

Vous remarquez la présence de "Moq": "4.6.38-alpha".

VI-D. Écriture des tests unitaires

Voici la première méthode pour laquelle nous allons écrire des tests :

 
Sélectionnez
 // GET: Students
       public async Task<IActionResult> Index()
       {
           return View(await _studentsRepository.GetAll());
       }

La méthode de test que nous allons écrire doit permettre de vérifier que le ViewResult contient la liste d'éléments qui a été retournée par le repository.

Nous allons premièrement créer un objet simulé de notre repository à partir de son interface :

 
Sélectionnez
var studentsRepositoryMock = new Mock<IStudentsRepository>();

Par la suite, nous allons changer le comportement de notre repository pour que lorsque la méthode GetAll() sera appelée dans notre méthode à tester, une autre méthode soit utilisée à la place :

 
Sélectionnez
studentsRepositoryMock.Setup(repo => repo.GetAll()).Returns(Task.FromResult(GetTestStudents()));

La méthode qui sera appelée à la place est GetTestStudents(), qui retourne une liste d'étudiants. Voici son code :

 
Sélectionnez
private IEnumerable<Student> GetTestStudents()
       {

           IEnumerable<Student> students = new List<Student>() {
           new Student {Id = 1, Email = "j.papavoisi@gmail.com", FirstName="Papavoisi", LastName="Jean" },
           new Student { Id = 2, Email = "p.garden@gmail.com", FirstName = "Garden", LastName = "Pierre" },
           new Student { Id = 3, Email = "r.derosi@gmail.com", FirstName = "Derosi", LastName = "Ronald" }
           };
           return students;
       }

Cela fait, nous allons passer l'instance de notre objet mocké au constructeur de StudentsController :

 
Sélectionnez
var controller = new StudentsController(studentsRepositoryMock.Object);

Par la suite, nous devons ajouter les assertions pour vérifier que le ViewResult retourne la liste d'éléments attendus :

 
Sélectionnez
Assert.IsNotNull(viewResult);
           var students = viewResult.ViewData.Model as List<Student>;
           Assert.AreEqual(3, students.Count);

Le code complet de notre méthode de test est le suivant :

 
Sélectionnez
     [TestMethod]
       public async Task Index_ReturnsAllStudents()
       {

           //Arrange
           var studentsRepositoryMock = new Mock<IStudentsRepository>();
           studentsRepositoryMock.Setup(repo => repo.GetAll()).Returns(Task.FromResult(GetTestStudents()));
           var controller = new StudentsController(studentsRepositoryMock.Object);

           // Act
           var viewResult = await controller.Index() as ViewResult;

           //assert
           Assert.IsNotNull(viewResult);
           var students = viewResult.ViewData.Model as List<Student>;
           Assert.AreEqual(3, students.Count);

       }

Pour la suite, nous allons rédiger les tests pour la méthode d'action Details :

 
Sélectionnez
public async Task<IActionResult> Details(int? id)
       {
           if (id == null)
           {
               return NotFound();
           }

           var student = await _studentsRepository.Find(id.Value);
           if (student == null)
           {
               return NotFound();
           }

           return View(student);
       }

Pour ce cas, nous allons rédiger un test qui permet de vérifier que le ViewResult contient un objet étudiant, et deux autres pour vérifier qu'un NotFound result est retourné.

Pour le premier cas, la méthode _studentsRepository.Find(id.Value) est appelée dans notre action. Nous allons donc configurer notre objet mocké pour retourner un étudiant lorsque cette méthode est appelée avec une valeur précise en paramètre :

 
Sélectionnez
 studentsRepositoryMock.Setup(repo => repo.Find(2)).Returns(Task.FromResult(GetTestStudents().ElementAt(1)));

On va faire une assertion pour vérifier que l'information attendue est contenue dans le ViewResult :

 
Sélectionnez
Assert.IsNotNull(viewResult);
           var student = viewResult.ViewData.Model as Student;
           Assert.AreEqual("Garden", student.FirstName);

Le code complet de la méthode de test est le suivant :

 
Sélectionnez
 [TestMethod]
       public async Task Details_ReturnsStudent()
       {

           //Arrange
           var studentsRepositoryMock = new Mock<IStudentsRepository>();

           studentsRepositoryMock.Setup(repo => repo.Find(2)).Returns(Task.FromResult(GetTestStudents().ElementAt(1)));
           var controller = new StudentsController(studentsRepositoryMock.Object);

           // Act
           var viewResult = await controller.Details(2) as ViewResult;

           //assert
           Assert.IsNotNull(viewResult);
           var student = viewResult.ViewData.Model as Student;
           Assert.AreEqual("Garden", student.FirstName);

       }

Pour le cas du NotFound result, nous avons deux cas de figure :

  • l'étudiant dont l'id a été spécifié n'a pas été trouvé ;
  • l'id passé est null.

Pour le premier cas, nous allons configurer notre objet mocké pour qu'il retourne null, lorsque la méthode Find() du repository est appelée avec la valeur « 2 » en paramètre :

 
Sélectionnez
 studentsRepositoryMock.Setup(repo => repo.Find(2)).Returns(Task.FromResult<Student>(null));

Ensuite, on fait une assertion pour vérifier qu'un NotFoundResult est retourné :

 
Sélectionnez
Assert.IsInstanceOfType(actionResult, typeof(NotFoundResult));

Le code complet :

 
Sélectionnez
[TestMethod]
       public async Task Details_ReturnsNotFoundWithId()
       {

           //Arrange
           var studentsRepositoryMock = new Mock<IStudentsRepository>();
           studentsRepositoryMock.Setup(repo => repo.Find(2)).Returns(Task.FromResult<Student>(null));
           var controller = new StudentsController(studentsRepositoryMock.Object);

           // Act
           IActionResult actionResult = await controller.Details(2) ;

           //assert
           Assert.IsInstanceOfType(actionResult, typeof(NotFoundResult));
          
       }

Pour le deuxième cas, nous n'aurons pas besoin de changer le comportement de notre objet mocké, car il ne sera pas appelé. Nous devons juste passer une valeur nulle à notre méthode d'action, ensuite vérifier qu'on obtient un NotFound result. Le code complet de cette méthode de test est le suivant :

 
Sélectionnez
[TestMethod]
       public async Task Details_ReturnsNotFoundWithNullId()
       {

           //Arrange
           var studentsRepositoryMock = new Mock<IStudentsRepository>();
           var controller = new StudentsController(studentsRepositoryMock.Object);

           // Act
           IActionResult actionResult = await controller.Details(null);

           //assert
           Assert.IsInstanceOfType(actionResult, typeof(NotFoundResult));

       }

Passons maintenant à la rédaction des tests unitaires pour la méthode d'action Create, dont voici le code :

 
Sélectionnez
 [HttpPost]
       [ValidateAntiForgeryToken]
       public async Task<IActionResult> Create([Bind("Id,Email,FirstName,LastName")] Student student)
       {
           if (ModelState.IsValid)
           {
               _studentsRepository.Add(student);
               await _studentsRepository.Save();
               return RedirectToAction("Index");
           }
           return View(student);
       }

Pour ce cas, nous allons rédiger deux tests :

  • l'un qui permettra de vérifier la redirection ;
  • l'autre pour le cas où le ModelState est invalide.

Pour le premier cas, le code de la méthode de test permettant d'effectuer cela est le suivant :

 
Sélectionnez
 [TestMethod]
       public async Task Create_ReturnsRedirectToAction()
       {

           //Arrange
           var studentsRepositoryMock = new Mock<IStudentsRepository>();
           var controller = new StudentsController(studentsRepositoryMock.Object);

           // Act
           var result = await controller.Create(new Student { Id=4, Email="a.Damien@gmail.com", FirstName="Damien", LastName="Alain" }) as RedirectToActionResult;

           //assert
           Assert.AreEqual("Index", result.ActionName);
       }

Pour le second cas, nous devons modifier notre contrôleur pour que son model state soit invalide :

 
Sélectionnez
controller.ModelState.AddModelError("Email", "Required");

En effet, les tests unitaires se font sur une méthode isolée. L'appel de la méthode Create exécute uniquement cette dernière. De ce fait, il n'y a aucun passage au travers du pipeline ASP.NET MVC, qui devait s'occuper du binding du modèle et de la validation.

Le code complet pour notre méthode de test est le suivant :

 
Sélectionnez
[TestMethod]
       public async Task Create_InvalidModelState()
       {

           //Arrange
           var studentsRepositoryMock = new Mock<IStudentsRepository>();
           var controller = new StudentsController(studentsRepositoryMock.Object);

           // Act
           controller.ModelState.AddModelError("Email", "Required");
           var result = await controller.Create(new Student ()) as ViewResult;

           var student = result.Model as Student;

           //assert
           Assert.IsNotNull(student);
       }

Á l'exécution, vous obtenez le résultat suivant :

Image non disponible

Je crois qu'avec ces quelques exemples, j'ai couvert différents scénarios pour les tests unitaires mockés d'un contrôleur avec des actions CRUD. Vous devez être en mesure d'écrire sans beaucoup d'effort les tests pour couvrir les autres méthodes d'action.

VII. Partie 3 : tests d'intégration avec Entity Framework Core InMemory

Avec les mocks, le développeur peut tester une unité de traitement (une méthode), sans avoir besoin de se soucier des dépendances avec d'autres classes. En isolant la méthode à tester, il est rassuré que si le test échoue, la cause réside dans le code et pas ailleurs.

Toutefois, dans le cycle de développement, le développeur va arriver à une phase où il aura besoin de tester au complet une fonctionnalité qui fait intervenir plusieurs unités de traitement. Á ce stade, on parle couramment de test d'intégration.

Prenons l'exemple d'une application ASP.NET MVC qui utilise Entity Framework et une base de données SQL Server. Pour effectuer des tests d'intégration, sans avoir à impacter la base de données existante, le développeur va mettre des efforts sur la duplication de son « contexte », qui sera utilisé pour les tests.

Entity Framework Core apporte le concept de base de données en mémoire (InMemory). Le provider InMemory permet de tester des composants en simulant un accès à la base de données comme dans un contexte d'utilisation réelle, sans toutefois impacter la base de données existante. De plus, cette option réduit les efforts pour mettre en œuvre le mocking.

VII-A. Ajout du package EntityFrameworkCore.InMemory

La première chose à faire sera d'ajouter une référence au package « Microsoft.EntityFrameworkCore.InMemory » dans le projet de test. Tapez la commande suivante dans la console NuGet :

 
Sélectionnez
Install-Package Microsoft.EntityFrameworkCore.InMemory

VII-B. Écriture des méthodes de tests

Vous allez ajouter au projet de test un nouveau fichier .CS. Pour commencer, nous devons créer une méthode qui va permettre de définir les options du DbContext (DbContextOptions).

 
Sélectionnez
private static DbContextOptions<SampleAppContext> CreateNewContextOptions()
       {    
  
      }

Dans cette méthode, nous allons dans un premier temps créer un nouveau ServiceProvider, qui va entraîner la génération d'une nouvelle instance d'une base de données InMemory.

 
Sélectionnez
var serviceProvider = new ServiceCollection()
               .AddEntityFrameworkInMemoryDatabase()
               .BuildServiceProvider();

Ensuite, nous allons créer une nouvelle instance du DbContextOptions, qui va permettre de spécifier à notre DbContext que nous souhaitons utiliser une base de données InMemory et notre nouveau serviceProvider. Le code pour effectuer cela est le suivant :

 
Sélectionnez
var builder = new DbContextOptionsBuilder<SampleAppContext>();
           builder.UseInMemoryDatabase()
                  .UseInternalServiceProvider(serviceProvider);

Pour finir, nous allons retourner nos nouvelles options pour notre DbContext :

 
Sélectionnez
return builder.Options;

Le code complet de cette méthode est le suivant :

 
Sélectionnez
private static DbContextOptions<SampleAppContext> CreateNewContextOptions()
       {
           
           var serviceProvider = new ServiceCollection()
               .AddEntityFrameworkInMemoryDatabase()
               .BuildServiceProvider();

          
           var builder = new DbContextOptionsBuilder<SampleAppContext>();
           builder.UseInMemoryDatabase()
                  .UseInternalServiceProvider(serviceProvider);

           return builder.Options;
       }

Dans notre stratégie de test, nous souhaitons que chaque méthode de test s'exécute avec une base de données InMemory contenant un certain nombre d'informations. Pour cela, nous devons ajouter à notre test une méthode d'initialisation ayant l'attribut [TestInitialize]

:

 
Sélectionnez
[TestInitialize]
      public async Task Init()
       {

}

Dans cette méthode, nous allons écrire le code permettant d'initialiser notre base de données InMemory.

 
Sélectionnez
[TestInitialize]
      public async Task Init()
       {
                    
            var  options = CreateNewContextOptions();

           _studentRepository = new StudentsRepository(new SampleAppContext(options));

           // var service = new StudentsRepository(context);
            _studentRepository.Add(new Student { Id = 1, Email = "j.papavoisi@gmail.com", FirstName = "Papavoisi", LastName = "Jean" });
            _studentRepository.Add(new Student { Id = 2, Email = "p.garden@gmail.com", FirstName = "Garden", LastName = "Pierre" });
            _studentRepository.Add(new Student { Id = 3, Email = "r.derosi@gmail.com", FirstName = "Derosi", LastName = "Ronald" });

           await _studentRepository.Save();
         
       }

Passons maintenant à l'écriture de nos méthodes de test. Nous allons reprendre le contrôleur de notre exemple précèdent. J'écrirais juste quelques méthodes de test pour illustrer l'utilisation de InMemory de Entity Framework.

Commençons par la méthode d'action Index(), qui retourne la liste des étudiants :

 
Sélectionnez
public async Task<IActionResult> Index()
       {
           return View(await _studentsRepository.GetAll());
       }

Le code de test pour cette dernière est le suivant :

 
Sélectionnez
[TestMethod]
       public async Task Index_ReturnsAllStudentsIn()
       {

           //Arrange
           var controller = new StudentsController(_studentRepository);

           // Act
           var viewResult = await controller.Index() as ViewResult;

           //assert
           Assert.IsNotNull(viewResult);
           var students = viewResult.ViewData.Model as List<Student>;
           Assert.AreEqual(3, students.Count);

       }

Passons à la méthode d'action Details :

 
Sélectionnez
public async Task<IActionResult> Details(int? id)
       {
           if (id == null)
           {
               return NotFound();
           }

           var student = await _studentsRepository.Find(id.Value);
           if (student == null)
           {
               return NotFound();
           }

           return View(student);
       }

Le code pour tester ce dernier avec un id qui existe dans la base de données est le suivant :

 
Sélectionnez
[TestMethod]
       public async Task Details_ReturnStudentIn()
       {

           //Arrange
           var controller = new StudentsController(_studentRepository);

           // Act
           var viewResult = await controller.Details(2) as ViewResult;

           //assert
           Assert.IsNotNull(viewResult);
           var student = viewResult.ViewData.Model as Student;
           Assert.AreEqual("Garden", student.FirstName);

       }

Enfin, nous allons écrire le code pour tester la méthode d'action Create() :

 
Sélectionnez
public async Task<IActionResult> Create([Bind("Id,Email,FirstName,LastName")] Student student)
       {
           if (ModelState.IsValid)
           {
               _studentsRepository.Add(student);
               await _studentsRepository.Save();
               return RedirectToAction("Index");
           }
           return View(student);
       }

Pour ce dernier cas, voici le code de la méthode de test correspondante :

 
Sélectionnez
[TestMethod]
       public async Task Create_ReturnsRedirectToActionIn()
       {

           //Arrange
           var controller = new StudentsController(_studentRepository);

           // Act
           var result = await controller.Create(new Student {Id = 4, Email = "a.Damien@gmail.com", FirstName = "Damien", LastName = "Alain" }) as RedirectToActionResult;

           //assert
           Assert.IsNotNull(result);
           Assert.AreEqual("Index", result.ActionName);
       }

Pour finir, ci-dessous le code complet de notre classe de test :

 
Sélectionnez
[TestClass]
   public class StudentsControllerTestIN
   {

       private IStudentsRepository _studentRepository;

       private static DbContextOptions<SampleAppContext> CreateNewContextOptions()
       {
           
           var serviceProvider = new ServiceCollection()
               .AddEntityFrameworkInMemoryDatabase()
               .BuildServiceProvider();

          
           var builder = new DbContextOptionsBuilder<SampleAppContext>();
           builder.UseInMemoryDatabase()
                  .UseInternalServiceProvider(serviceProvider);

           return builder.Options;
       }

      [TestInitialize]
      public async Task Init()
       {
           
           
            var  options = CreateNewContextOptions();

           _studentRepository = new StudentsRepository(new SampleAppContext(options));

           // var service = new StudentsRepository(context);
            _studentRepository.Add(new Student { Id = 1, Email = "j.papavoisi@gmail.com", FirstName = "Papavoisi", LastName = "Jean" });
            _studentRepository.Add(new Student { Id = 2, Email = "p.garden@gmail.com", FirstName = "Garden", LastName = "Pierre" });
            _studentRepository.Add(new Student { Id = 3, Email = "r.derosi@gmail.com", FirstName = "Derosi", LastName = "Ronald" });

           await _studentRepository.Save();
         

       }

       [TestMethod]
       public async Task Index_ReturnsAllStudentsIn()
       {

           //Arrange
           var controller = new StudentsController(_studentRepository);

           // Act
           var viewResult = await controller.Index() as ViewResult;

           //assert
           Assert.IsNotNull(viewResult);
           var students = viewResult.ViewData.Model as List<Student>;
           Assert.AreEqual(3, students.Count);

       }

       [TestMethod]
       public async Task Details_ReturnStudentIn()
       {

           //Arrange
           var controller = new StudentsController(_studentRepository);

           // Act
           var viewResult = await controller.Details(2) as ViewResult;

           //assert
           Assert.IsNotNull(viewResult);
           var student = viewResult.ViewData.Model as Student;
           Assert.AreEqual("Garden", student.FirstName);

       }

       [TestMethod]
       public async Task Create_ReturnsRedirectToActionIn()
       {

           //Arrange
           var controller = new StudentsController(_studentRepository);

           // Act
           var result = await controller.Create(new Student {Id = 4, Email = "a.Damien@gmail.com", FirstName = "Damien", LastName = "Alain" }) as RedirectToActionResult;

           //assert
           Assert.IsNotNull(result);
           Assert.AreEqual("Index", result.ActionName);
       }

   }

Á l'exécution, vous obtenez le résultat suivant :

Image non disponible

VIII. Conclusion

Grâce aux tests unitaires, vous pouvez améliorer la qualité de votre application et faciliter la maintenance de cette dernière. Suite à la lecture de cet article, vous serez désormais en mesure d'écrire des tests unitaires pour une application ASP.NET Core, en utilisant diverses techniques.

IX. Remerciements

Je tiens à remercier milkoseckmilkoseck pour sa relecture orthographique.

X. Références

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2016 Hinault Romaric. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.