Voir le flux RSS

Blog de Hinault Romaric (.NET Core, ASP.NET Core, Azure, DevOps)

[Actualité] Tests unitaires d’une application ASP.NET Core Razor Pages

Noter ce billet
par , 18/11/2017 à 15h50 (607 Affichages)
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.

Dans mon précédent billet de blog, j’ai présenté comment créer une application CRUD en utilisant Razor Pages, Visual Studio Code et Entity Framework Core. La structure d’une application Razor Pages est très différente de celle d’une application MVC. De ce fait, la mise en place des tests unitaires varie également.

Dans ce billet, nous verrons comment mettre en place des tests unitaires pour une application CRUD Razor Pages. Nous allons utiliser Entity Framework InMemory pour mocker la base de données.

Vous pouvez télécharger le projet de démarrage sur ma page GitHub. Vous devez disposer de .Net Core 2.0 et d’un éditeur de code, notamment Visual Studio Code ou SublimeText.

Création du projet de test

Nous allons utiliser la plateforme de test de Microsoft MsTest. La première chose à faire sera de créer le projet de test unitaire. Le projet de test unitaire doit être créé dans le même dossier parent que celui du projet RazorDemo.

Nom : img1.png
Affichages : 992
Taille : 2,4 Ko

Pour créer le projet de test, vous allez exécuter la commande suivante dans le dossier parent des projets :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
Dotnet new mstest -n RazorDemoTest
Une fois le projet créé, vous devez ajouter une référence au projet RazorDemo. Pour cela, vous devez éditer le fichier RazorDemoTest.csproj et ajouter une référence au projet RazorDemo. Il s’agit de renseigner le chemin vers le fichier RazorDemo.csproj :

Code xml : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
<ItemGroup>
    <ProjectReference Include="..\RazorDemo\RazorDemo.csproj" />
  </ItemGroup>

Vous pouvez également exécuter la commande dotnet add reference pour ajouter la référence au projet RazorDemo :

Code : Sélectionner tout - Visualiser dans une fenêtre à part
dotnet add reference ../RazorDemo/RazorDemo.csproj
Le fichier RazorDemoTest.csproj devrait ressembler à ceci :

Code xml : 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
<Project Sdk="Microsoft.NET.Sdk">
 
  <PropertyGroup>
    <TargetFramework>netcoreapp2.0</TargetFramework>
 
    <IsPackable>false</IsPackable>
  </PropertyGroup>
 
  <ItemGroup>
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.3.0-preview-20170628-02" />
    <PackageReference Include="MSTest.TestAdapter" Version="1.1.18" />
    <PackageReference Include="MSTest.TestFramework" Version="1.1.18" />
  </ItemGroup>
 
  <ItemGroup>
    <ProjectReference Include="..\RazorDemo\RazorDemo.csproj" />
  </ItemGroup>
 
</Project>

Création de la classe de base

Nous aurons besoin d’une classe de base qui sera héritée par nos tests unitaires. Cette classe implémentera le code pour créer une instance de la base de données InMemory qui sera utilisée par nos tests unitaires.

Vous allez donc créer à la racine du projet de test le fichier BaseTest.cs et ajouter les références nécessaires. Cette classe doit disposer d’une propriété de type RazorDemoContext, pouvant être utilisée dans les classes enfants :

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
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RazorDemo.Models;
using System.Threading.Tasks;
 
namespace RazorDemoTest
{
    [TestClass]
    public class BaseTest
    {
 
        protected RazorDemoContext Context;
    }
}

Vous allez créer dans cette classe 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<RazorDemoContext> CreateNewContextOptions()
        {
 
        }

Dans cette méthode, vous allez créer un nouveau ServiceProvider, qui va entraîner 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, vous allez 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 ayant pour nom « InMemoryDb » 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<RazorDemoContext>();
            builder.UseInMemoryDatabase(databaseName: "InMemoryDb")
                   .UseInternalServiceProvider(serviceProvider);

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

Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
     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
private static DbContextOptions<RazorDemoContext> CreateNewContextOptions()
        {
 
            var serviceProvider = new ServiceCollection()
                .AddEntityFrameworkInMemoryDatabase()
                .BuildServiceProvider();
 
            var builder = new DbContextOptionsBuilder<RazorDemoContext>();
            builder.UseInMemoryDatabase(databaseName: "InMemoryDb")
                   .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]. 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
 [TestInitialize]
        public async Task Init()
        {
 
            var options = CreateNewContextOptions();
            Context = new RazorDemoContext(options);
 
 
            Context.Add(new Student { Id = 1, Email = "j.papavoisi@gmail.com", FirstName = "Papavoisi", LastName = "Jean" });
            Context.Add(new Student { Id = 2, Email = "p.garden@gmail.com", FirstName = "Garden", LastName = "Pierre" });
            Context.Add(new Student { Id = 3, Email = "r.derosi@gmail.com", FirstName = "Derosi", LastName = "Ronald" });
 
            await Context.SaveChangesAsync();
        }

Le code complet de la classe BaseTest 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
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
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using RazorDemo.Models;
using System.Threading.Tasks;
 
namespace RazorDemoTest
{
    [TestClass]
    public class BaseTest
    {
 
        protected RazorDemoContext Context;
 
        private static DbContextOptions<RazorDemoContext> CreateNewContextOptions()
        {
 
            var serviceProvider = new ServiceCollection()
                .AddEntityFrameworkInMemoryDatabase()
                .BuildServiceProvider();
 
            var builder = new DbContextOptionsBuilder<RazorDemoContext>();
            builder.UseInMemoryDatabase(databaseName: "InMemoryDb")
                   .UseInternalServiceProvider(serviceProvider);
 
            return builder.Options;
        }
 
        [TestInitialize]
        public async Task Init()
        {
 
            var options = CreateNewContextOptions();
            Context = new RazorDemoContext(options);
 
 
            Context.Add(new Student { Id = 1, Email = "j.papavoisi@gmail.com", FirstName = "Papavoisi", LastName = "Jean" });
            Context.Add(new Student { Id = 2, Email = "p.garden@gmail.com", FirstName = "Garden", LastName = "Pierre" });
            Context.Add(new Student { Id = 3, Email = "r.derosi@gmail.com", FirstName = "Derosi", LastName = "Ronald" });
 
            await Context.SaveChangesAsync();
        }
 
    }
}

Écriture des tests unitaires

Avant d’écrire nos tests unitaires, nous devons respecter la même structure que le projet à tester :

Nom : img2.png
Affichages : 1028
Taille : 10,3 Ko

Nous allons donc commencer par créer un dossier Pages, ensuite un dossier Students dans ce dossier.

Test de la classe IndeModel

Nous allons écrire le test unitaire pour tester la classe IndeModel contenue dans le fichier Index.cshtml.cs. Le code de la classe IndexModel 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
17
18
19
20
21
22
23
24
25
26
27
28
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.EntityFrameworkCore;
using RazorDemo.Models;
 
namespace RazorDemo.Pages.Students
{
    public class IndexModel : PageModel
    {
        private readonly RazorDemo.Models.RazorDemoContext _context;
 
        public IndexModel(RazorDemo.Models.RazorDemoContext context)
        {
            _context = context;
        }
 
        public IList<Student> Student { get;set; }
 
        public async Task OnGetAsync()
        {
            Student = await _context.Student.ToListAsync();
        }
    }
}

Nous allons donc créer un fichier IndexTest.cs dans le dossier Students. La classe IndexTest doit hériter de BaseTest :

Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
   [TestClass]
    public class IndexTest : BaseTest
    {
 
    }

Nous allons écrire le code de test pour la méthode OnGetAsync(). Nous devons dans un premier temps initialiser un nouvel objet IndexModel, en lui passant en paramètre notre DBContext mocké avec Entity Framework Core InMemory :

Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
            //Arrange
            var indexModel = new IndexModel(Context);

Ensuite, nous allons procéder à l’exécution de la méthode OnGetAsync() :

Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
   //Act
            await indexModel.OnGetAsync();

Enfin, nous allons mettre en place nos assertions pour vérifier nos résultats :

Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
        //Assert
            var students = indexModel.Student;
            Assert.IsNotNull(students);
            Assert.AreEqual(3, students.Count);

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
15
  [TestMethod]
        public async Task OnGetAsync_ReturnAllStudents()
        {
            //Arrange
            var indexModel = new IndexModel(Context);
 
            //Act
            await indexModel.OnGetAsync();
 
            //Assert
            var students = indexModel.Student;
            Assert.IsNotNull(students);
            Assert.AreEqual(3, students.Count);
 
        }

Nous allons suivre le même principe pour les prochains tests.

Pour exécuter votre test, vous pouvez simplement utiliser la commande Dotnet test dans le dossier du projet de test. Cette commande va builder le projet RazorDemo, ensuite le projet RazorDemoTest, avant d’exécuter les tests unitaires présents :

Nom : img3.png
Affichages : 974
Taille : 16,1 Ko

Test de la Classe CreateModel

Passons à l’écriture des tests pour la classe CreateModel. Nous allons comme pour le test précédent, créer dans le dossier correspondant le fichier CreateTest.cs. La classe CreateTest doit hériter de BaseTest.

Le code pour lequel nous allons écrire les tests unitaires 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
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class CreateModel : PageModel
    {
        private readonly RazorDemo.Models.RazorDemoContext _context;
 
        public CreateModel(RazorDemo.Models.RazorDemoContext context)
        {
            _context = context;
        }
 
        public IActionResult OnGet()
        {
            return Page();
        }
 
        [BindProperty]
        public Student Student { get; set; }
 
        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
 
            _context.Student.Add(Student);
            await _context.SaveChangesAsync();
 
            return RedirectToPage("./Index");
        }
    }

Pour ce code, nous avons trois tests à écrire. Un pour la méthode OnGet() et deux pour la méthode OnPostAsync().

Le test pour la méthode OnGet() doit juste vérifier qu’un PageResult est retourné. Son code 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
  [TestMethod]
        public void OnGet_ReturnPageResult()
        {  
            //Arrange
            var createModel = new CreateModel(Context);
 
            //Act
            var page = createModel.OnGet() as PageResult;
 
            //Assert
            Assert.IsNotNull(page);
 
        }

Pour la méthode OnPostAsync(), on va dans un premier temps écrire un test qui simule un échec de la validation du Model :

Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
   if (!ModelState.IsValid)
            {
                return Page();
            }

Pour cela, nous devons initialiser la propriété PageContext du PageModel (dans la prochaine version de ASP.NET Core, il ne sera plus nécessaire d’initialiser le PageContext. Pour plus de détails, voir ce billet de blog que j’ai rédigé). Ensuite, ajouter une erreur de validation au ModelStateDictionary :

Code c# : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
  createModel.PageContext = new PageContext();
                createModel.ModelState.AddModelError("FirstName", "Required");

Le code complet de cette méthode de test 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
[TestMethod]
            public async Task OnPostAsync_ReturnPageResult()
            {
                //Arrange
                var createModel = new CreateModel(Context);
                createModel.Student = new RazorDemo.Models.Student();
                createModel.PageContext = new PageContext();
                createModel.ModelState.AddModelError("FirstName", "Required");
 
                //Act
                var page = await createModel.OnPostAsync() as PageResult;
 
                //Assert
                Assert.IsNotNull(page);
            }

La seconde méthode de test que nous allons écrire permettra d’enregistrer un étudiant, avant de nous rediriger vers la page Index :

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
[TestMethod]
        public async Task OnPostAsync_ReturnRedirectToPageResult()
        {
            //Arrange
            var createModel = new CreateModel(Context);
            createModel.Student = new RazorDemo.Models.Student() { Id = 4,
                                                                  FirstName ="Thomas",
                                                                  LastName="Larabi",
                                                                 Email = "Thomas.Larabi@gmail.com"};
            createModel.PageContext = new PageContext();
 
            //Act
            var redirect = await createModel.OnPostAsync() as RedirectToPageResult;
 
            //Assert
            Assert.IsNotNull(redirect);
            Assert.AreEqual(redirect.PageName, "./Index");
        }

Test de DetailsModel


Le code à tester pour la classe DetailsModel 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
17
18
19
20
21
22
23
24
25
26
27
public class DetailsModel : PageModel
    {
        private readonly RazorDemo.Models.RazorDemoContext _context;
 
        public DetailsModel(RazorDemo.Models.RazorDemoContext context)
        {
            _context = context;
        }
 
        public Student Student { get; set; }
 
        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }
 
            Student = await _context.Student.SingleOrDefaultAsync(m => m.Id == id);
 
            if (Student == null)
            {
                return NotFound();
            }
            return Page();
        }
}

Pour la méthode OnGetAsync(), nous allons écrire trois tests unitaires. Un pour le return Page et deux autres tests pour les deux return NotFound() que nous avons dans notre code.

Vous devez donc créer le fichier de test correspondant (DetailsTest) dans le dossier correspondant. Les trois tests à écrire sont les suivants :

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
        [TestMethod]
        public async Task OnGetAsync_ReturnPage()
        {
            //Arrange
            var detailsModel = new DetailsModel(Context);
 
            //Act
            var page = await detailsModel.OnGetAsync(3) as PageResult;
 
            //Assert
            Assert.IsNotNull(page);
            var student = detailsModel.Student;
            Assert.IsNotNull(student);
            Assert.AreEqual(3, student.Id);
            Assert.AreEqual("Derosi", student.FirstName);
            Assert.AreEqual("Ronald", student.LastName);
            Assert.AreEqual("r.derosi@gmail.com", student.Email);
 
        }
 
        [TestMethod]
        public async Task OnGetAsync_ReturnNotFound_WithNullId()
        {
            //Arrange
            var detailsModel = new DetailsModel(Context);
 
            //Act
            IActionResult actionResult =  await detailsModel.OnGetAsync(null);
 
            //Assert
            Assert.IsInstanceOfType(actionResult, typeof(NotFoundResult));
            Assert.IsNull(detailsModel.Student);
        }
 
 
        [TestMethod]
        public async Task OnGetAsync_ReturnNotFound_WithId()
        {
            //Arrange
            var detailsModel = new DetailsModel(Context);
 
            //Act
            IActionResult actionResult = await detailsModel.OnGetAsync(6);
 
            //Assert
            Assert.IsInstanceOfType(actionResult, typeof(NotFoundResult));
            Assert.IsNull(detailsModel.Student);
        }

Test de EditModel


Le code à tester pour la classe EditModel 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
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
public class EditModel : PageModel
    {
        private readonly RazorDemo.Models.RazorDemoContext _context;
 
        public EditModel(RazorDemo.Models.RazorDemoContext context)
        {
            _context = context;
        }
 
        [BindProperty]
        public Student Student { get; set; }
 
        public async Task<IActionResult> OnGetAsync(int? id)
        {
            if (id == null)
            {
                return NotFound();
            }
 
            Student = await _context.Student.SingleOrDefaultAsync(m => m.Id == id);
 
            if (Student == null)
            {
                return NotFound();
            }
            return Page();
        }
 
        public async Task<IActionResult> OnPostAsync()
        {
            if (!ModelState.IsValid)
            {
                return Page();
            }
 
            _context.Attach(Student).State = EntityState.Modified;
 
            try
            {
                await _context.SaveChangesAsync();
            }
            catch (DbUpdateConcurrencyException)
            {
 
            }
 
            return RedirectToPage("./Index");
        }
    }

La méthode OnGetAsync() étant similaire à la méthode du même nom dans la classe DetailsModel, elle nécessitera des tests similaires. Je ne vais donc pas revenir dessus.

Nous allons passer directement à l’écriture des tests pour la méthode OnPostAsync(). Nous aurons besoin de deux tests pour couvrir cette méthode. Avec ce que nous avons appris suite à l’écriture des tests pour la classe CreateModel, nous ne devons pas avoir de difficulté pour écrire ces deux tests.

Le code pour ces deux méthodes de test 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
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 [TestMethod]
        public async Task OnPostAsync_ReturnPageResult()
        {
            //Arrange
            var editModel = new EditModel(Context);
            editModel.Student = new RazorDemo.Models.Student();
            editModel.PageContext = new PageContext();
            editModel.ModelState.AddModelError("FirstName", "Required");
 
            //Act
            var page = await editModel.OnPostAsync() as PageResult;
 
            //Assert
            Assert.IsNotNull(page);
        }
 
 
        [TestMethod]
        public async Task OnPostAsync_ReturnRedirectToPageResult()
        {
            //Arrange
            var editModel = new EditModel(Context);
            editModel.Student = await Context.Student.SingleOrDefaultAsync(m => m.Id == 3);
            editModel.Student.FirstName = "Jean";
            editModel.PageContext = new PageContext();
 
            //Act
            var redirect = await editModel.OnPostAsync() as RedirectToPageResult;
 
            //Assert
            Assert.IsNotNull(redirect);
            Assert.AreEqual(redirect.PageName, "./Index");
        }

Test de la classe DeleteModel

À partir de ce que vous avez appris dans ce billet de blog, je vous laisse le soin d’écrire les tests pour la classe DeleteModel.

Le code complet du projet à tester et des tests unitaires est disponible sur ma page GitHub.

Bon coding!

Envoyer le billet « Tests unitaires d’une application ASP.NET Core Razor Pages » dans le blog Viadeo Envoyer le billet « Tests unitaires d’une application ASP.NET Core Razor Pages » dans le blog Twitter Envoyer le billet « Tests unitaires d’une application ASP.NET Core Razor Pages » dans le blog Google Envoyer le billet « Tests unitaires d’une application ASP.NET Core Razor Pages » dans le blog Facebook Envoyer le billet « Tests unitaires d’une application ASP.NET Core Razor Pages » dans le blog Digg Envoyer le billet « Tests unitaires d’une application ASP.NET Core Razor Pages » dans le blog Delicious Envoyer le billet « Tests unitaires d’une application ASP.NET Core Razor Pages » dans le blog MySpace Envoyer le billet « Tests unitaires d’une application ASP.NET Core Razor Pages » dans le blog Yahoo

Mis à jour 18/11/2017 à 18h18 par Malick

Catégories
DotNET , C# , .NET Core , ASP.NET Core , Tests Unitaires

Commentaires