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
| Code xml : | Sélectionner tout |
1 2 3 | <ItemGroup> <ProjectReference Include="..\RazorDemo\RazorDemo.csproj" /> </ItemGroup> |
| Code : | Sélectionner tout |
dotnet add reference ../RazorDemo/RazorDemo.csproj
| 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> |
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; } } |
| Code c# : | Sélectionner tout |
1 2 3 4 | private static DbContextOptions<RazorDemoContext> CreateNewContextOptions() { } |
| Code c# : | Sélectionner tout |
1 2 3 | var serviceProvider = new ServiceCollection() .AddEntityFrameworkInMemoryDatabase() .BuildServiceProvider(); |
| Code c# : | Sélectionner tout |
1 2 3 | var builder = new DbContextOptionsBuilder<RazorDemoContext>(); builder.UseInMemoryDatabase(databaseName: "InMemoryDb") .UseInternalServiceProvider(serviceProvider); |
| Code c# : | Sélectionner tout |
return builder.Options;
| 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; } |
| 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(); } |
| 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(); } } } |
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(); } } } |
| Code c# : | Sélectionner tout |
1 2 3 4 5 | [TestClass] public class IndexTest : BaseTest { } |
| Code c# : | Sélectionner tout |
1 2 | //Arrange var indexModel = new IndexModel(Context); |
| Code c# : | Sélectionner tout |
1 2 | //Act await indexModel.OnGetAsync(); |
| Code c# : | Sélectionner tout |
1 2 3 4 | //Assert var students = indexModel.Student; Assert.IsNotNull(students); Assert.AreEqual(3, students.Count); |
| 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); } |
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"); } } |
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); } |
| Code c# : | Sélectionner tout |
1 2 3 4 | if (!ModelState.IsValid) { return Page(); } |
| Code c# : | Sélectionner tout |
1 2 | createModel.PageContext = new PageContext(); createModel.ModelState.AddModelError("FirstName", "Required"); |
| 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); } |
| 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"); } |
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(); } } |
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"); } } |
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"); } |
À 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 lu gratuitement 3 313 articles depuis plus d'un an.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.
Soutenez le club developpez.com en souscrivant un abonnement pour que nous puissions continuer à vous proposer des publications.