3-2 Tests unitaires (MVC)
Test unitaire simple pour la page d'accueil
-
Sous
Snowfall.Tests/Web.Mvc/TestsUnitaires
->Add
->Class/Interface
-
Nommer la classe
AccueilTests
-
Créer la fonction de test
Obtenir_PageAccueil_RetourneViewResult
Snowfall.Tests/Web.Mvc/TestsUnitaires/AccueilTests.cs[Fact]
public async Task Obtenir_PageAccueil_RetourneViewResult()
{
} -
Ensuite, on simule les dépendances du contrôleur
EvenementsController
Snowfall.Tests/Web.Mvc/TestsUnitaires/AccueilTests.cs[Fact]
public async Task Obtenir_PageAccueil_RetourneViewResult()
{
// Arrange
var evenements = new List<Evenement>
{
new Evenement
{
Id = 1,
Nom = "Evenement",
Capacite = 100,
Date = DateTime.Today,
Description = "Description",
Prix = new Decimal(9.99),
VilleId = 1
}
};
var villes = new List<Ville>
{
new Ville
{
Id = 1,
Nom = "Vegas",
PaysIso = "US"
}
};
var mockEvenementService = new Mock<IEvenementService>();
var mockVilleService = new Mock<IVilleService>();
mockEvenementService.Setup(x => x.FindByVilleId(It.IsAny<int>())).ReturnsAsync(evenements);
mockEvenementService.Setup(x => x.GetAll()).ReturnsAsync(evenements);
mockVilleService.Setup(x => x.GetAll()).ReturnsAsync(villes);
}infoLe contrôleur
EvenementsController
prend en argument dans son constructeur deux services:IEvenementService
etIVilleService
. Ces derniers communiquent avec la base de données qui n'existe pas en mode tests unitaires.On crée un mock via
new Mock<Type>
. C'est ici que d'avoir utilisé des interfaces est particulièrement utile puisqu'on vient associer à l'interfaceIEvenementService
par exemple, une instance fictive.Ensuite, on
mock
les fonctions requises par l'action du contrôleur en retournant des résultats viaSetup
.Par exemple:
mockEvenementService.Setup(x => x.FindByVilleId(It.IsAny<int>())).ReturnsAsync(evenements);
dit que pour la fonctionFindByVilleId
, en recevant n'importe quelint
en paramètre, retournera la liste d'événements codée du dur dans la fonction. -
Puis, on instancie le contrôleur (
EvenementsController
) et on appelle la fonction désirée (Index
).Snowfall.Tests/Web.Mvc/TestsUnitaires/AccueilTests.cs[Fact]
public async Task Obtenir_PageAccueil_RetourneViewResult()
{
// Arrange
var evenements = new List<Evenement>
{
new Evenement
{
Id = 1,
Nom = "Evenement",
Capacite = 100,
Date = DateTime.Today,
Description = "Description",
Prix = new Decimal(9.99),
VilleId = 1
}
};
var villes = new List<Ville>
{
new Ville
{
Id = 1,
Nom = "Vegas",
PaysIso = "US"
}
};
var mockEvenementService = new Mock<IEvenementService>();
var mockVilleService = new Mock<IVilleService>();
mockEvenementService.Setup(x => x.FindByVilleId(It.IsAny<int>())).ReturnsAsync(evenements);
mockEvenementService.Setup(x => x.GetAll()).ReturnsAsync(evenements);
mockVilleService.Setup(x => x.GetAll()).ReturnsAsync(villes);
// Act
var controller = new EvenementsController(mockEvenementService.Object, mockVilleService.Object);
var resultat = (ViewResult) await controller.Index(null);
}infoPour retourner une instance de l'objet simulé, on appelle
.Object
sur lemock
. -
On termine par vérifier que le résultat reçu est de type
ViewResult
, que leViewModel
retourné est de typeEvenementIndexViewModel
et qu'un seul item est présent dans la liste d'événements et de villes duViewModel
.Snowfall.Tests/Web.Mvc/TestsUnitaires/AccueilTests.cs[Fact]
public async Task Obtenir_PageAccueil_RetourneViewResult()
{
// Arrange
var evenements = new List<Evenement>
{
new Evenement
{
Id = 1,
Nom = "Evenement",
Capacite = 100,
Date = DateTime.Today,
Description = "Description",
Prix = new Decimal(9.99),
VilleId = 1
}
};
var villes = new List<Ville>
{
new Ville
{
Id = 1,
Nom = "Vegas",
PaysIso = "US"
}
};
var mockEvenementService = new Mock<IEvenementService>();
var mockVilleService = new Mock<IVilleService>();
mockEvenementService.Setup(x => x.FindByVilleId(It.IsAny<int>())).ReturnsAsync(evenements);
mockEvenementService.Setup(x => x.GetAll()).ReturnsAsync(evenements);
mockVilleService.Setup(x => x.GetAll()).ReturnsAsync(villes);
// Act
var controller = new EvenementsController(mockEvenementService.Object, mockVilleService.Object);
var resultat = (ViewResult) await controller.Index(null);
// Assert
Assert.IsType<ViewResult>(resultat);
var viewModel = resultat.ViewData.Model;
Assert.IsType<EvenementsIndexViewModel>(viewModel);
Assert.Single((viewModel as EvenementsIndexViewModel)!.Evenements);
Assert.Single((viewModel as EvenementsIndexViewModel)!.FiltresEvenements.Villes);
}
Test unitaire de création de comptes (création avec dépendances Identity)
Si vous avez une dépendance à Identity
dans un test, il faut créer des mock
un peu plus costaud. Voici un exemple de test de création de compte s'appuyant sur Identity.
- Sous
Snowfall.Tests/Web.Mvc/TestsUnitaires
->Add
->Class/Interface
- Nommer la classe
ComptesTests
- Ajouter la fonction de test
Creer_CompteValide_RetourneRedirectIndexEvenements()
Snowfall.Tests/Web.Mvc/TestsUnitaires/ComptesTests.cspublic class ComptesTests
{
[Fact]
public async Task Creer_CompteValide_RetourneRedirectIndexEvenements()
{
var userManagerMock = new Mock<UserManager<ApplicationUser>>(
/* IUserStore<TUser> store */Mock.Of<IUserStore<ApplicationUser>>(),
/* IOptions<IdentityOptions> optionsAccessor */null,
/* IPasswordHasher<TUser> passwordHasher */null,
/* IEnumerable<IUserValidator<TUser>> userValidators */null,
/* IEnumerable<IPasswordValidator<TUser>> passwordValidators */null,
/* ILookupNormalizer keyNormalizer */null,
/* IdentityErrorDescriber errors */null,
/* IServiceProvider services */null,
/* ILogger<UserManager<TUser>> logger */null);
var signInManagerMock = new Mock<SignInManager<ApplicationUser>>(
userManagerMock.Object,
/* IHttpContextAccessor contextAccessor */Mock.Of<IHttpContextAccessor>(),
/* IUserClaimsPrincipalFactory<TUser> claimsFactory */Mock.Of<IUserClaimsPrincipalFactory<ApplicationUser>>(),
/* IOptions<IdentityOptions> optionsAccessor */null,
/* ILogger<SignInManager<TUser>> logger */null,
/* IAuthenticationSchemeProvider schemes */null,
/* IUserConfirmation<TUser> confirmation */null);
userManagerMock
.Setup(x => x.FindByNameAsync(It.IsAny<string>()))
.ReturnsAsync((ApplicationUser?)null);
userManagerMock
.Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>()))
.ReturnsAsync(IdentityResult.Success);
signInManagerMock
.Setup(x => x.PasswordSignInAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<bool>(), It.IsAny<bool>()))
.ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.Success);
var comptesController = new ComptesController(userManagerMock.Object, signInManagerMock.Object);
CreerCompteViewModel creerCompteViewModel = CreerCompteViewModel();
var resultat = await comptesController.Create(creerCompteViewModel);
Assert.IsType<RedirectToActionResult>(resultat);
Assert.Equal("Index", (resultat as RedirectToActionResult)!.ActionName);
Assert.Equal("Evenements", (resultat as RedirectToActionResult)!.ControllerName);
}
private CreerCompteViewModel CreerCompteViewModel()
{
return new CreerCompteViewModel()
{
Prenom = "Benoit",
Nom = "Tremblay",
Adresse = "sdfsdfsdfsdf",
CodePostal = "A0A 0A0",
ConfirmPassword = "!Allo122432",
Email = "u@ser.com",
Password = "!Allo122432",
Province = "QC",
Ville = "Las Vegas",
Pays = "US"
};
}
}
Support pour les validations dans les tests unitaires
Un aspect négatif des tests unitaires MVC est que les validations ne sont pas exécuté automatiquement comme dans un test d'intégration. En effet, les validations sur les modèles sont exécutées dans un Middleware
avant d'arriver au contrôleur.
Ainsi, il faut rouler les validations manuellement. On peut ajouter une petite classe utilitaire pour cela:
- Sous le dossier
Snowfall.Tests/Helpers
->Add
->Class/Interface
- Nommer la classe
ValidationHelper
- Ajouter le code suivant à la classe
Snowfall.Tests/Helpers/ValidationHelper.cs
namespace Snowfall.Tests.Helpers;
using System.ComponentModel.DataAnnotations;
using Microsoft.AspNetCore.Mvc.ModelBinding;
public static class ValidationHelper
{
public static void ValidateModel(object model, ModelStateDictionary modelState)
{
var validationContext = new ValidationContext(model, serviceProvider: null, items: null);
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(model, validationContext, validationResults, true);
foreach (var validationResult in validationResults)
{
var memberName = validationResult.MemberNames.FirstOrDefault() ?? string.Empty;
if (validationResult.ErrorMessage != null)
modelState.AddModelError(memberName, validationResult.ErrorMessage);
}
}
}
Modifier le test unitaire de création de comptes pour utiliser les validations
Le test unitaire peut ensuite être modifié afin de faire usage des validations.
[Fact]
public async Task Creer_CompteValide_RetourneRedirectIndexEvenements()
{
var userManagerMock = new Mock<UserManager<ApplicationUser>>(
/* IUserStore<TUser> store */Mock.Of<IUserStore<ApplicationUser>>(),
/* IOptions<IdentityOptions> optionsAccessor */null,
/* IPasswordHasher<TUser> passwordHasher */null,
/* IEnumerable<IUserValidator<TUser>> userValidators */null,
/* IEnumerable<IPasswordValidator<TUser>> passwordValidators */null,
/* ILookupNormalizer keyNormalizer */null,
/* IdentityErrorDescriber errors */null,
/* IServiceProvider services */null,
/* ILogger<UserManager<TUser>> logger */null);
var signInManagerMock = new Mock<SignInManager<ApplicationUser>>(
userManagerMock.Object,
/* IHttpContextAccessor contextAccessor */Mock.Of<IHttpContextAccessor>(),
/* IUserClaimsPrincipalFactory<TUser> claimsFactory */
Mock.Of<IUserClaimsPrincipalFactory<ApplicationUser>>(),
/* IOptions<IdentityOptions> optionsAccessor */null,
/* ILogger<SignInManager<TUser>> logger */null,
/* IAuthenticationSchemeProvider schemes */null,
/* IUserConfirmation<TUser> confirmation */null);
userManagerMock
.Setup(x => x.FindByNameAsync(It.IsAny<string>()))
.ReturnsAsync((ApplicationUser?)null);
userManagerMock
.Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>()))
.ReturnsAsync(IdentityResult.Success);
signInManagerMock
.Setup(x => x.PasswordSignInAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<bool>(),
It.IsAny<bool>()))
.ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.Success);
var comptesController = new ComptesController(userManagerMock.Object, signInManagerMock.Object);
CreerCompteViewModel creerCompteViewModel = CreerCompteViewModel();
ValidationHelper.ValidateModel(creerCompteViewModel, comptesController.ModelState);
var resultat = await comptesController.Create(creerCompteViewModel);
Assert.IsType<RedirectToActionResult>(resultat);
Assert.Equal("Index", (resultat as RedirectToActionResult)!.ActionName);
Assert.Equal("Evenements", (resultat as RedirectToActionResult)!.ControllerName);
}
On fait appel à ValidationHelper.ValidateModel
en passant en argument le modèle à valider (creerCompteViewModel
), puis le ModelState
du contrôleur auquel assigné le résultat des validations.
Test unitaire de création de comptes non valide
Pour tester que les validations fonctionnent bien dans le contrôleur, essayons avec un ViewModel
qui ne contient pas d'adresse courriel.
- Créez une nouvelle fonction de test
Snowfall.Tests/Web.Mvc/TestsUnitaires/ComptesTests.cs
[Fact]
public async Task Creer_CompteNonValide_RetourneErreurValidation()
{
} - Assignez une valeur valide vide à un des champs requis
Snowfall.Tests/Web.Mvc/TestsUnitaires/ComptesTests.cs
[Fact]
public async Task Creer_CompteNonValide_RetourneErreurValidation()
{
var userManagerMock = new Mock<UserManager<ApplicationUser>>(
/* IUserStore<TUser> store */Mock.Of<IUserStore<ApplicationUser>>(),
/* IOptions<IdentityOptions> optionsAccessor */null,
/* IPasswordHasher<TUser> passwordHasher */null,
/* IEnumerable<IUserValidator<TUser>> userValidators */null,
/* IEnumerable<IPasswordValidator<TUser>> passwordValidators */null,
/* ILookupNormalizer keyNormalizer */null,
/* IdentityErrorDescriber errors */null,
/* IServiceProvider services */null,
/* ILogger<UserManager<TUser>> logger */null);
var signInManagerMock = new Mock<SignInManager<ApplicationUser>>(
userManagerMock.Object,
/* IHttpContextAccessor contextAccessor */Mock.Of<IHttpContextAccessor>(),
/* IUserClaimsPrincipalFactory<TUser> claimsFactory */
Mock.Of<IUserClaimsPrincipalFactory<ApplicationUser>>(),
/* IOptions<IdentityOptions> optionsAccessor */null,
/* ILogger<SignInManager<TUser>> logger */null,
/* IAuthenticationSchemeProvider schemes */null,
/* IUserConfirmation<TUser> confirmation */null);
userManagerMock
.Setup(x => x.FindByNameAsync(It.IsAny<string>()))
.ReturnsAsync((ApplicationUser?)null);
userManagerMock
.Setup(x => x.CreateAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>()))
.ReturnsAsync(IdentityResult.Success);
signInManagerMock
.Setup(x => x.PasswordSignInAsync(It.IsAny<ApplicationUser>(), It.IsAny<string>(), It.IsAny<bool>(),
It.IsAny<bool>()))
.ReturnsAsync(Microsoft.AspNetCore.Identity.SignInResult.Success);
var comptesController = new ComptesController(userManagerMock.Object, signInManagerMock.Object);
CreerCompteViewModel creerCompteViewModel = CreerCompteViewModel();
creerCompteViewModel.Email = String.Empty;
ValidationHelper.ValidateModel(creerCompteViewModel, comptesController.ModelState);
var resultat = await comptesController.Create(creerCompteViewModel);
Assert.False(comptesController.ModelState.IsValid);
Assert.IsType<ViewResult>(resultat);
Assert.Equal("New", (resultat as ViewResult)!.ViewName);
}