Aller au contenu principal

3-2 Tests unitaires (MVC)

Test unitaire simple pour la page d'accueil

  1. Sous Snowfall.Tests/Web.Mvc/TestsUnitaires -> Add -> Class/Interface

  2. Nommer la classe AccueilTests

  3. Créer la fonction de test Obtenir_PageAccueil_RetourneViewResult

    Snowfall.Tests/Web.Mvc/TestsUnitaires/AccueilTests.cs
    [Fact]
    public async Task Obtenir_PageAccueil_RetourneViewResult()
    {


    }
  4. 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);
    }
    info

    Le contrôleur EvenementsController prend en argument dans son constructeur deux services: IEvenementService et IVilleService. 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'interface IEvenementService par exemple, une instance fictive.

    Ensuite, on mock les fonctions requises par l'action du contrôleur en retournant des résultats via Setup.

    Par exemple: mockEvenementService.Setup(x => x.FindByVilleId(It.IsAny<int>())).ReturnsAsync(evenements); dit que pour la fonction FindByVilleId, en recevant n'importe quel int en paramètre, retournera la liste d'événements codée du dur dans la fonction.

  5. 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);
    }
    info

    Pour retourner une instance de l'objet simulé, on appelle .Object sur le mock.

  6. On termine par vérifier que le résultat reçu est de type ViewResult, que le ViewModel retourné est de type EvenementIndexViewModel et qu'un seul item est présent dans la liste d'événements et de villes du ViewModel.

    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.

  1. Sous Snowfall.Tests/Web.Mvc/TestsUnitaires -> Add -> Class/Interface
  2. Nommer la classe ComptesTests
  3. Ajouter la fonction de test Creer_CompteValide_RetourneRedirectIndexEvenements()
    Snowfall.Tests/Web.Mvc/TestsUnitaires/ComptesTests.cs
    public 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:

  1. Sous le dossier Snowfall.Tests/Helpers -> Add -> Class/Interface
  2. Nommer la classe ValidationHelper
  3. 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.

Snowfall.Tests/Web.Mvc/TestsUnitaires/ComptesTests.cs
[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);
}
info

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.

  1. Créez une nouvelle fonction de test
    Snowfall.Tests/Web.Mvc/TestsUnitaires/ComptesTests.cs
    [Fact]
    public async Task Creer_CompteNonValide_RetourneErreurValidation()
    {

    }
  2. 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);
    }