Apprendre à mettre en place des tests unitaires pour une application CRUD Razor Pages
Par Hinault Romaric

Le , par Hinault Romaric, Responsable .NET
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.


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

Code : Sélectionner tout
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
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
dotnet add reference ../RazorDemo/RazorDemo.csproj
Le fichier RazorDemoTest.csproj devrait ressembler à ceci :

Code xml : Sélectionner tout
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
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
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
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
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
     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
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
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
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 :


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
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
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
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
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
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
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 :



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
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
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
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
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
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
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
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
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
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
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!


Vous avez aimé cette actualité ? Alors partagez-la avec vos amis en cliquant sur les boutons ci-dessous :
Contacter le responsable de la rubrique Accueil