Dans mon précédent billet, j’ai présenté comment vous pouvez écrire des tests unitaires mockés pour une application ASP.NET Core en utilisant MsTest V2 et Moq.
Les objets mockés permettent aux développeurs de créer des objets simulés qui reproduisent le comportement désiré des objets réels, à leur invocation. 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. A 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.
Trêve de bavardage. Passons à la pratique.
Nous allons reprendre notre application d’exemple du précédent billet. 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.
Code : | Sélectionner tout |
Install-Package Microsoft.EntityFrameworkCore.InMemory
Code c# : | Sélectionner tout |
1 2 3 4 | private static DbContextOptions<SampleAppContext> CreateNewContextOptions() { } |
Dans cette méthode, nous allons dans un premier temps créer un nouveau ServiceProvider, qui va entrainer la génération d’une nouvelle instance d’une base de données InMemory.
Code c# : | Sélectionner tout |
1 2 3 | 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 :
Code c# : | Sélectionner tout |
1 2 3 | var builder = new DbContextOptionsBuilder<SampleAppContext>(); builder.UseInMemoryDatabase() .UseInternalServiceProvider(serviceProvider); |
Pour finir, nous allons retourner nos nouvelles options pour notre DbContext :
Code c# : | Sélectionner tout |
1 2 |
return builder.Options; |
Le code complet de cette méthode est le suivant :
Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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]
:
Code c# : | Sélectionner tout |
1 2 3 4 5 | [TestInitialize] public async Task Init() { } |
Dans cette méthode, nous allons écrire le code permettant d’initialiser notre base de données InMemory.
Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | [TestInitialize] public async Task Init() { var options = CreateNewContextOptions(); _studentRepository = new StudentsRepository(new SampleAppContext(options)); _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. Je prends pour prérequis le fait que vous avez lu mon billet de blog précédent. Donc, je vais m’abstenir d’entrer dans les détails, et j’écrirais juste quelques méthodes de test.
Commençons par la méthode d’action Index(), qui retourne la liste des étudiants :
Code c# : | Sélectionner tout |
1 2 3 4 | public async Task<IActionResult> Index() { return View(await _studentsRepository.GetAll()); } |
Le code de test pour cette dernière est le suivant :
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 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 :
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); } |
Le code pour tester ce dernier avec un id qui existe dans la base de données est le suivant :
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_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() :
Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | 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 :
Code c# : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | [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 tests :
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 | [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)); _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); } } |
Au travers de ce billet de blog, vous avez découvert comment utiliser la fonctionnalité InMemory pour effectuer des tests en simulant un accès à votre base de données comme dans un contexte d’utilisation réelle, sans toutefois impacter votre base de données. De plus, l’utilisation de InMemory est pratique dans les situations où il faut de gros efforts pour mettre en œuvre le mocking.