Dans mon précèdent billet de blog, j’ai présenté MsTest V2, la nouvelle version du framework de tests unitaires de Microsoft. Cette version est encore au stade de preview. Elle supporte le framework .NET Core. Dans ce billet, nous avons vu comment intégrer MsTest V2 à un projet et écrire des tests unitaires pour une application ASP.NET MVC Core.Dans ce nouveau billet de blog, nous irons un peu plus loin et mous verrons comment écrire des tests unitaires mockés avec MsTest V2 et Moq.
Petit 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 ne charge de .NET core.
Ajout du package Moq au projet
Nous allons reprendre notre projet de tests du billet de blog précèdent. La première chose à faire sera d’installer le package Moq dans le projet de tests en utilisant la console NuGet. La commande à utiliser est la suivante :
| Code : | Sélectionner tout |
Install-Package Moq -Pre
| Code json : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | { "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" } } } } } |
Le contrôleur à tester
Nous allons tester un contrôleur qui dispose d’actions CRUD. Le code complet de ce contrôleur est le suivant :
| Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 | using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.EntityFrameworkCore; using SampleApp.Models; using SampleApp.Repository; namespace SampleApp.Controllers { 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) { await _studentsRepository.Add(student); 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 { await _studentsRepository.Update(student); } 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); return RedirectToAction("Index"); } } } |
Le code que nous devons 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 constructeur. Vous verrez combien cela va faciliter l’écriture de nos tests unitaires mockés.
Trêve de bavardage. Passons à la pratique.
Écriture des tests unitaires
Voici la première méthode pour laquelle nous allons écrire un test :
| Code c# : | Sélectionner tout |
1 2 3 4 5 | // GET: Students public async Task<IActionResult> Index() { return View(await _studentsRepository.GetAll()); } |
Nous allons premièrement créer un objet simulé de notre repository à partir de son interface :
| Code c# : | Sélectionner tout |
var studentsRepositoryMock = new Mock<IStudentsRepository>();
| Code c# : | Sélectionner tout |
studentsRepositoryMock.Setup(repo => repo.GetAll()).Returns(Task.FromResult(GetTestStudents()));
| Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | 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; } |
| Code c# : | Sélectionner tout |
var controller = new StudentsController(studentsRepositoryMock.Object);
| Code c# : | Sélectionner tout |
1 2 3 | Assert.IsNotNull(viewResult); var students = viewResult.ViewData.Model as List<Student>; Assert.AreEqual(3, students.Count); |
| Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | [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); } |
| Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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 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 :
| Code c# : | Sélectionner tout |
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 :
| Code c# : | Sélectionner tout |
1 2 3 | Assert.IsNotNull(viewResult); var student = viewResult.ViewData.Model as Student; Assert.AreEqual("Garden", student.FirstName); |
| Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | [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); } |
- -l’etudiant 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 nul, lorsque la méthode Find() du repository est appelée avec la valeur “2” en paramètre :
| Code c# : | Sélectionner tout |
studentsRepositoryMock.Setup(repo => repo.Find(2)).Returns(Task.FromResult<Student>(null));
Ensuite, on fait une assertion pour vérifier qu’un NotFoundResult est retourné :
| Code : | Sélectionner tout |
=c#Assert.IsInstanceOfType(actionResult, typeof(NotFoundResult));
| Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | [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)); } |
| Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | [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)); } |
| Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create([Bind("Id,Email,FirstName,LastName")] Student student) { if (ModelState.IsValid) { await _studentsRepository.Add(student); return RedirectToAction("Index"); } return View(student); } |
- 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 :
| Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | [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.IsNotNull(result); Assert.AreEqual("Index", result.ActionName); } |
| Code : | Sélectionner tout |
controller.ModelState.AddModelError("Email", "Required");
Le code complet pour notre méthode de test est le suivant :
| Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public async Task Create_InvalidModelState() { //Arrange var studentsRepositoryMock = new Mock<IStudentsRepository>(); var controller = new StudentsController(studentsRepositoryMock.Object); // Act controller.ModelState.AddModelError("Email", "Required"); var viewResult = await controller.Create(new Student ()) as ViewResult; //assert Assert.IsNotNull(viewResult); var student = viewResult.Model as Student; Assert.IsNotNull(student); } |
| Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 | [TestClass] public class StudentsControllerTest { [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); } [TestMethod] public async Task Details_ReturnStudent() { //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); } [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)); } [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)); } [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.IsNotNull(result); Assert.AreEqual("Index", result.ActionName); } [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 viewResult = await controller.Create(new Student ()) as ViewResult; //assert Assert.IsNotNull(viewResult); var student = viewResult.Model as Student; Assert.IsNotNull(student); } 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; } } |
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.
Dans mon prochain billet, nous verrons comment rédiger des tests d’intégration en exploitant la fonctionnalité InMemory de Entity Framework Core. Restez connecter !
Vous avez lu gratuitement 4 034 articles depuis plus d'un an.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.