Voir le flux RSS

Hinault Romaric

[Actualité] Entity Framework Core : tests d’integration avec InMemory

Note : 5 votes pour une moyenne de 5,00.
par , 04/09/2016 à 20h40 (1254 Affichages)
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
1
2
 
return builder.Options;

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

Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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 - Visualiser dans une fenêtre à part
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.

Envoyer le billet « Entity Framework Core : tests d’integration avec  InMemory » dans le blog Viadeo Envoyer le billet « Entity Framework Core : tests d’integration avec  InMemory » dans le blog Twitter Envoyer le billet « Entity Framework Core : tests d’integration avec  InMemory » dans le blog Google Envoyer le billet « Entity Framework Core : tests d’integration avec  InMemory » dans le blog Facebook Envoyer le billet « Entity Framework Core : tests d’integration avec  InMemory » dans le blog Digg Envoyer le billet « Entity Framework Core : tests d’integration avec  InMemory » dans le blog Delicious Envoyer le billet « Entity Framework Core : tests d’integration avec  InMemory » dans le blog MySpace Envoyer le billet « Entity Framework Core : tests d’integration avec  InMemory » dans le blog Yahoo

Commentaires

  1. Avatar de François DORIN
    • |
    • permalink
    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...
  2. Avatar de Hinault Romaric
    • |
    • permalink
    Citation Envoyé par dorinf
    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.