Entity Framework Core : apprendre à faire des tests d'integration avec InMemory dans une application ASP.NET MVC,
Un tutoriel de Hinault Romaric

Le , par Hinault Romaric, Responsable .NET
Ce nouveau billet de blog s’inscrit dans la même thématique que le précédent : les tests.

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
Pour commencer, nous devons créer une méthode qui va permettre de définir les options du DbContext (DbContextOptions).

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.


Vous avez aimé cette actualité ? Alors partagez-la avec vos amis en cliquant sur les boutons ci-dessous :


 Poster un commentaire

Avatar de François DORIN François DORIN - Rédacteur/Modérateur https://www.developpez.com
le 04/09/2016 à 22:41
Bonsoir,

Encore une fois un bon billet

Une autre approche à l'utilisation de InMemory pour réaliser les tests d'intégration consiste à utiliser des transactions, et à effectuer un rollback à la fin de chaque test. Mais cette méthode est sans doute un peu plus intrusive...
Avatar de Hinault Romaric Hinault Romaric - Responsable .NET https://www.developpez.com
le 07/09/2016 à 23:39
Citation Envoyé par dorinf;bt3074
Bonsoir,

Encore une fois un bon billet

Une autre approche à l'utilisation de InMemory pour réaliser les tests d'intégration consiste à utiliser des transactions, et à effectuer un rollback à la fin de chaque test. Mais cette méthode est sans doute un peu plus intrusive...

Je n'ai pas encore évalué cette éventualité. Mais, ca me semble demander un peu plus d'effort au développeur.
Offres d'emploi IT
Responsable de projet (calculateur moteur) H/F
Safran - Ile de France - Massy (91300)
Ingénieur conception électrique / électronique H/F
Safran - Ile de France - Villaroche
Expert Technico Fonctionnel Sharepoint H/F
Safran - Ile de France - Corbeil (91)

Voir plus d'offres Voir la carte des offres IT
Contacter le responsable de la rubrique Accueil