par , 18/11/2017 à 15h50 (2933 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.
Pour créer le projet de test, vous allez exécuter la commande suivante dans le dossier parent des projets :
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 :
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 :
dotnet add reference ../RazorDemo/RazorDemo.csproj
Le fichier RazorDemoTest.csproj devrait ressembler à ceci :
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 :
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).
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.
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 :
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 :
Le code complet de cette méthode est le suivant :
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.
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 :
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 :
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 :
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 :
1 2
| //Arrange
var indexModel = new IndexModel(Context); |
Ensuite, nous allons procéder à l’exécution de la méthode OnGetAsync() :
1 2
| //Act
await indexModel.OnGetAsync(); |
Enfin, nous allons mettre en place nos assertions pour vérifier nos résultats :
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 :
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 :
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 :
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 :
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 :
1 2
| createModel.PageContext = new PageContext();
createModel.ModelState.AddModelError("FirstName", "Required"); |
Le code complet de cette méthode de test est le suivant :
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 :
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 :
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 :
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 :
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 :
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!