Tests unitaires de pages Razor dans ASP.NET Core

ASP.NET Core prend en charge les tests unitaires d’applications Razor Pages. Les tests de la couche d’accès aux données (DAL) et des modèles de page permettent de s’assurer que :

  • Les parties d’une application Razor Pages fonctionnent indépendamment et ensemble en tant qu’unité pendant la construction de l’application.
  • Les classes et méthodes ont des étendues de responsabilité limitées.
  • Il existe une documentation supplémentaire sur le comportement de l’application.
  • Vous rencontrez les régressions, qui sont des erreurs provoquées par les mises à jour du code, lors de la génération et du déploiement automatisés.

Cette rubrique suppose que vous avez une compréhension de base des applications Razor Pages et des tests unitaires. Si vous n’êtes pas familiarisé avec les applications Razor Pages ou les concepts de test, consultez les rubriques suivantes :

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

L’exemple de projet se compose de deux applications :

Application Dossier du projet Description
Application de message src/RazorPagesTestSample Permet à un utilisateur d’ajouter un message, de supprimer un message, de supprimer tous les messages et d’analyser les messages (rechercher le nombre moyen de mots par message).
Tester une application tests/RazorPagesTestSample.Tests Permet de tester unitairement la couche d’accès aux données et le modèle de page Index de l’application de message.

Les tests peuvent être exécutés à l’aide des fonctionnalités de test intégrées d’un environnement de développement intégré, telles que Visual Studio. Si vous utilisez Visual Studio Code ou la ligne de commande, exécutez la commande suivante à une invite de commandes dans le dossier tests/RazorPagesTestSample.Tests :

dotnet test

Organisation de l’application de message

L’application de message est un système de messages Razor Pages avec les caractéristiques suivantes :

  • La page Index de l’application (Pages/Index.cshtml et Pages/Index.cshtml.cs) fournit une interface utilisateur et des méthodes de modèle de page pour contrôler l’ajout, la suppression et l’analyse des messages (recherche du nombre moyen de mots par message).
  • Un message est décrit par la classe Message (Data/Message.cs) avec deux propriétés : Id (clé) et Text (message). La propriété Text est obligatoire et limitée à 200 caractères.
  • Les messages sont stockés à l’aide de la base de données en mémoire d’Entity Framework†.
  • L’application contient une couche d’accès aux données dans sa classe de contexte de base de données, AppDbContext (Data/AppDbContext.cs). Les méthodes de la couche d’accès aux données sont marquées virtual, ce qui permet de simuler les méthodes à utiliser dans les tests.
  • Si la base de données est vide au démarrage de l’application, la banque de messages est initialisée avec trois messages. Ces messages amorcés sont également utilisés dans les tests.

†La rubrique EF, Tester avec InMemory, explique comment utiliser une base de données en mémoire pour les tests avec MSTest. Cette rubrique utilise l’infrastructure de test xUnit. Les concepts de test et les implémentations de test entre différents frameworks de test sont similaires, mais pas identiques.

Bien que l’exemple d’application n’utilise pas le modèle de référentiel et ne soit pas un exemple efficace du modèle d’unité de travail (UoW), Razor Pages prend en charge ces modèles de développement. Pour plus d’informations, consultez Conception de la couche de persistance de l’infrastructure et Tester la logique du contrôleur dans ASP.NET Core (l’exemple implémente le modèle de référentiel).

Tester l’organisation de l’application

L’application de test est une application console dans le dossier tests/RazorPagesTestSample.Tests.

Dossier d’application de test Description
UnitTests
  • DataAccessLayerTest.cs contient les tests unitaires pour la couche d’accès aux données.
  • IndexPageTests.cs contient les tests unitaires pour le modèle de page Index.
Utilitaires Contient la méthode TestDbContextOptions utilisée pour créer de nouvelles options de contexte de base de données pour chaque test unitaire de la couche d’accès aux données afin que la base de données soit réinitialisée à sa condition de base de référence pour chaque test.

Le framework de test est xUnit. Le framework de simulation d’objet est Moq.

Tests unitaires de la couche d’accès aux données (DAL)

L’application de message a une couche d’accès aux données avec quatre méthodes contenues dans la classe AppDbContext (src/RazorPagesTestSample/Data/AppDbContext.cs). Chaque méthode a un ou deux tests unitaires dans l’application de test.

Méthode de couche d’accès aux données Fonction
GetMessagesAsync Obtient un List<Message> à partir de la base de données triée sur la propriété Text.
AddMessageAsync Ajoute un Message à la base de données.
DeleteAllMessagesAsync Supprime toutes les entrées Message de la base de données.
DeleteMessageAsync Supprime un seul Message de la base de données par Id.

Les tests unitaires de la couche d’accès aux données nécessitent DbContextOptions lors de la création d’un nouveau AppDbContext pour chaque test. Une approche de la création de DbContextOptions pour chaque test consiste à utiliser un DbContextOptionsBuilder :

var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
    .UseInMemoryDatabase("InMemoryDb");

using (var db = new AppDbContext(optionsBuilder.Options))
{
    // Use the db here in the unit test.
}

Le problème avec cette approche est que chaque test reçoit la base de données dans l’état laissé par le test précédent. Cela peut être problématique lorsque vous essayez d’écrire des tests unitaires atomiques qui n’interfèrent pas les uns avec les autres. Pour forcer le AppDbContext à utiliser un nouveau contexte de base de données pour chaque test, fournissez une instance DbContextOptions basée sur un nouveau fournisseur de services. L’application de test montre comment procéder à l’aide de la méthode TestDbContextOptions de sa classe Utilities (tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs) :

public static DbContextOptions<AppDbContext> TestDbContextOptions()
{
    // Create a new service provider to create a new in-memory database.
    var serviceProvider = new ServiceCollection()
        .AddEntityFrameworkInMemoryDatabase()
        .BuildServiceProvider();

    // Create a new options instance using an in-memory database and 
    // IServiceProvider that the context should resolve all of its 
    // services from.
    var builder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb")
        .UseInternalServiceProvider(serviceProvider);

    return builder.Options;
}

L’utilisation du DbContextOptions dans les tests unitaires de couche d’accès aux données permet à chaque test de s’exécuter atomiquement avec une nouvelle instance de base de données :

using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
    // Use the db here in the unit test.
}

Chaque méthode de test de la classe DataAccessLayerTest (UnitTests/DataAccessLayerTest.cs) suit un modèle Organisation, Action, Assertion similaire :

  1. Organisation : la base de données est configurée pour le test et/ou le résultat attendu est défini.
  2. Action : le test est exécuté.
  3. Assertion : des assertions sont effectuées pour déterminer si le résultat du test est un succès.

Par exemple, la méthode DeleteMessageAsync est chargée de supprimer un message unique identifié par son Id (src/RazorPagesTestSample/Data/AppDbContext.cs) :

public async virtual Task DeleteMessageAsync(int id)
{
    var message = await Messages.FindAsync(id);

    if (message != null)
    {
        Messages.Remove(message);
        await SaveChangesAsync();
    }
}

Il existe deux tests pour cette méthode. Un test vérifie que la méthode supprime un message lorsque le message est présent dans la base de données. L’autre méthode teste que la base de données ne change pas si le message à supprimer (Id) n’existe pas. Voici la méthode DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound :

[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var seedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(seedMessages);
        await db.SaveChangesAsync();
        var recId = 1;
        var expectedMessages = 
            seedMessages.Where(message => message.Id != recId).ToList();

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

Tout d’abord, la méthode effectue l’étape Organisation, où la préparation de l’étape Action a lieu. Les messages d’amorçage sont obtenus et conservés dans seedMessages. Les messages d’amorçage sont enregistrés dans la base de données. Le message avec un Id de 1 est défini pour suppression. Lorsque la méthode DeleteMessageAsync est exécutée, les messages attendus doivent comprendre tous les messages à l’exception de celui avec un Id de 1. La variable expectedMessages représente ce résultat attendu.

// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages = 
    seedMessages.Where(message => message.Id != recId).ToList();

La méthode agit : la méthode DeleteMessageAsync est exécutée en passant le recId de 1 :

// Act
await db.DeleteMessageAsync(recId);

Enfin, la méthode obtient Messages à partir du contexte et le compare à expectedMessages pour affirmer que les deux sont égaux :

// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

Afin de comparer que les deux List<Message> pour déterminer s’ils sont égaux :

  • Les messages sont classés par Id.
  • Les paires de messages sont comparées sur la propriété Text.

Une méthode de test similaire, DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound, vérifie le résultat de la tentative de suppression d’un message qui n’existe pas. Dans ce cas, les messages attendus dans la base de données doivent être égaux aux messages réels après l’exécution de la méthode DeleteMessageAsync. Le contenu de la base de données ne doit pas être modifié :

[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var expectedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(expectedMessages);
        await db.SaveChangesAsync();
        var recId = 4;

        // Act
        try
        {
            await db.DeleteMessageAsync(recId);
        }
        catch
        {
            // recId doesn't exist
        }

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

Tests unitaires des méthodes de modèle de page

Un autre ensemble de tests unitaires est responsable des tests des méthodes de modèle de page. Dans l’application de message, les modèles de page Index se trouvent dans la classe IndexModel dans src/RazorPagesTestSample/Pages/Index.cshtml.cs.

Méthode de modèle de page Fonction
OnGetAsync Obtient les messages de la couche d’accès aux données pour l’interface utilisateur à l’aide de la méthode GetMessagesAsync.
OnPostAddMessageAsync Si ModelState est valide, appelle AddMessageAsync pour ajouter un message à la base de données.
OnPostDeleteAllMessagesAsync Appelle DeleteAllMessagesAsync pour supprimer tous les messages de la base de données.
OnPostDeleteMessageAsync Exécute DeleteMessageAsync pour supprimer un message avec la valeur Id spécifiée.
OnPostAnalyzeMessagesAsync Si un ou plusieurs messages se trouvent dans la base de données, calcule le nombre moyen de mots par message.

Les méthodes de modèle de page sont testées à l’aide de sept tests dans la classe IndexPageTests (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs). Les tests utilisent le modèle Organisation, Action, Assertion familier. Ces tests visent à :

  • Déterminer si les méthodes suivent le comportement correct lorsque ModelState n’est pas valide.
  • Confirmer que les méthodes produisent le bon IActionResult.
  • Vérifier que les affectations de valeurs de propriété sont effectuées correctement.

Ce groupe de tests simule souvent des méthodes de la couche d’accès aux données pour produire les données attendues pour l’étape Action où une méthode de modèle de page est exécutée. Par exemple, la méthode GetMessagesAsync de AppDbContext est simulée pour produire une sortie. Lorsqu’une méthode de modèle de page exécute cette méthode, la simulation retourne le résultat. Les données ne proviennent pas de la base de données. Cela crée des conditions de test prévisibles et fiables pour l’utilisation de la couche d’accès aux données dans les tests de modèle de page.

Le test OnGetAsync_PopulatesThePageModel_WithAListOfMessages montre comment la méthode GetMessagesAsync est simulée pour le modèle de page :

var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
    db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);

Lorsque la méthode OnGetAsync est exécutée à l’étape Action, elle appelle la méthode GetMessagesAsync du modèle de page.

Étape Action du test unitaire (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs) :

// Act
await pageModel.OnGetAsync();

Méthode OnGetAsync du modèle de page IndexPage (src/RazorPagesTestSample/Pages/Index.cshtml.cs) :

public async Task OnGetAsync()
{
    Messages = await _db.GetMessagesAsync();
}

La méthode GetMessagesAsync dans la couche d’accès aux données ne retourne pas le résultat de cet appel de méthode. La version simulée de la méthode retourne le résultat.

Dans l’étape Assert, les messages réels (actualMessages) sont attribués à partir de la propriété Messages du modèle de page. Une vérification de type est également effectuée lorsque les messages sont affectés. Les messages attendus et réels sont comparés sur leurs propriétés Text. Le test affirme que les deux instances List<Message> contiennent les mêmes messages.

// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

D’autres tests de ce groupe créent des objets de modèle de page qui incluent DefaultHttpContext, ModelStateDictionary, un ActionContext pour établir PageContext, un ViewDataDictionary et un PageContext. Ceux-ci sont utiles pour effectuer les tests. Par exemple, l’application de message établit une erreur ModelState avec AddModelError pour vérifier qu’un PageResult valide est retourné lorsque OnPostAddMessageAsync est exécuté :

[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
    // Arrange
    var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb");
    var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
    var expectedMessages = AppDbContext.GetSeedingMessages();
    mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
    var httpContext = new DefaultHttpContext();
    var modelState = new ModelStateDictionary();
    var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
    var modelMetadataProvider = new EmptyModelMetadataProvider();
    var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
    var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
    var pageContext = new PageContext(actionContext)
    {
        ViewData = viewData
    };
    var pageModel = new IndexModel(mockAppDbContext.Object)
    {
        PageContext = pageContext,
        TempData = tempData,
        Url = new UrlHelper(actionContext)
    };
    pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");

    // Act
    var result = await pageModel.OnPostAddMessageAsync();

    // Assert
    Assert.IsType<PageResult>(result);
}

Ressources supplémentaires

ASP.NET Core prend en charge les tests unitaires d’applications Razor Pages. Les tests de la couche d’accès aux données (DAL) et des modèles de page permettent de s’assurer que :

  • Les parties d’une application Razor Pages fonctionnent indépendamment et ensemble en tant qu’unité pendant la construction de l’application.
  • Les classes et méthodes ont des étendues de responsabilité limitées.
  • Il existe une documentation supplémentaire sur le comportement de l’application.
  • Vous rencontrez les régressions, qui sont des erreurs provoquées par les mises à jour du code, lors de la génération et du déploiement automatisés.

Cette rubrique suppose que vous avez une compréhension de base des applications Razor Pages et des tests unitaires. Si vous n’êtes pas familiarisé avec les applications Razor Pages ou les concepts de test, consultez les rubriques suivantes :

Affichez ou téléchargez l’exemple de code (procédure de téléchargement)

L’exemple de projet se compose de deux applications :

Application Dossier du projet Description
Application de message src/RazorPagesTestSample Permet à un utilisateur d’ajouter un message, de supprimer un message, de supprimer tous les messages et d’analyser les messages (rechercher le nombre moyen de mots par message).
Tester une application tests/RazorPagesTestSample.Tests Permet de tester unitairement la couche d’accès aux données et le modèle de page Index de l’application de message.

Les tests peuvent être exécutés à l’aide des fonctionnalités de test intégrées d’un environnement de développement intégré, telles que Visual Studio. Si vous utilisez Visual Studio Code ou la ligne de commande, exécutez la commande suivante à une invite de commandes dans le dossier tests/RazorPagesTestSample.Tests :

dotnet test

Organisation de l’application de message

L’application de message est un système de messages Razor Pages avec les caractéristiques suivantes :

  • La page Index de l’application (Pages/Index.cshtml et Pages/Index.cshtml.cs) fournit une interface utilisateur et des méthodes de modèle de page pour contrôler l’ajout, la suppression et l’analyse des messages (recherche du nombre moyen de mots par message).
  • Un message est décrit par la classe Message (Data/Message.cs) avec deux propriétés : Id (clé) et Text (message). La propriété Text est obligatoire et limitée à 200 caractères.
  • Les messages sont stockés à l’aide de la base de données en mémoire d’Entity Framework†.
  • L’application contient une couche d’accès aux données dans sa classe de contexte de base de données, AppDbContext (Data/AppDbContext.cs). Les méthodes de la couche d’accès aux données sont marquées virtual, ce qui permet de simuler les méthodes à utiliser dans les tests.
  • Si la base de données est vide au démarrage de l’application, la banque de messages est initialisée avec trois messages. Ces messages amorcés sont également utilisés dans les tests.

†La rubrique EF, Tester avec InMemory, explique comment utiliser une base de données en mémoire pour les tests avec MSTest. Cette rubrique utilise l’infrastructure de test xUnit. Les concepts de test et les implémentations de test entre différents frameworks de test sont similaires, mais pas identiques.

Bien que l’exemple d’application n’utilise pas le modèle de référentiel et ne soit pas un exemple efficace du modèle d’unité de travail (UoW), Razor Pages prend en charge ces modèles de développement. Pour plus d’informations, consultez Conception de la couche de persistance de l’infrastructure et Tester la logique du contrôleur dans ASP.NET Core (l’exemple implémente le modèle de référentiel).

Tester l’organisation de l’application

L’application de test est une application console dans le dossier tests/RazorPagesTestSample.Tests.

Dossier d’application de test Description
UnitTests
  • DataAccessLayerTest.cs contient les tests unitaires pour la couche d’accès aux données.
  • IndexPageTests.cs contient les tests unitaires pour le modèle de page Index.
Utilitaires Contient la méthode TestDbContextOptions utilisée pour créer de nouvelles options de contexte de base de données pour chaque test unitaire de la couche d’accès aux données afin que la base de données soit réinitialisée à sa condition de base de référence pour chaque test.

Le framework de test est xUnit. Le framework de simulation d’objet est Moq.

Tests unitaires de la couche d’accès aux données (DAL)

L’application de message a une couche d’accès aux données avec quatre méthodes contenues dans la classe AppDbContext (src/RazorPagesTestSample/Data/AppDbContext.cs). Chaque méthode a un ou deux tests unitaires dans l’application de test.

Méthode de couche d’accès aux données Fonction
GetMessagesAsync Obtient un List<Message> à partir de la base de données triée sur la propriété Text.
AddMessageAsync Ajoute un Message à la base de données.
DeleteAllMessagesAsync Supprime toutes les entrées Message de la base de données.
DeleteMessageAsync Supprime un seul Message de la base de données par Id.

Les tests unitaires de la couche d’accès aux données nécessitent DbContextOptions lors de la création d’un nouveau AppDbContext pour chaque test. Une approche de la création de DbContextOptions pour chaque test consiste à utiliser un DbContextOptionsBuilder :

var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
    .UseInMemoryDatabase("InMemoryDb");

using (var db = new AppDbContext(optionsBuilder.Options))
{
    // Use the db here in the unit test.
}

Le problème avec cette approche est que chaque test reçoit la base de données dans l’état laissé par le test précédent. Cela peut être problématique lorsque vous essayez d’écrire des tests unitaires atomiques qui n’interfèrent pas les uns avec les autres. Pour forcer le AppDbContext à utiliser un nouveau contexte de base de données pour chaque test, fournissez une instance DbContextOptions basée sur un nouveau fournisseur de services. L’application de test montre comment procéder à l’aide de la méthode TestDbContextOptions de sa classe Utilities (tests/RazorPagesTestSample.Tests/Utilities/Utilities.cs) :

public static DbContextOptions<AppDbContext> TestDbContextOptions()
{
    // Create a new service provider to create a new in-memory database.
    var serviceProvider = new ServiceCollection()
        .AddEntityFrameworkInMemoryDatabase()
        .BuildServiceProvider();

    // Create a new options instance using an in-memory database and 
    // IServiceProvider that the context should resolve all of its 
    // services from.
    var builder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb")
        .UseInternalServiceProvider(serviceProvider);

    return builder.Options;
}

L’utilisation du DbContextOptions dans les tests unitaires de couche d’accès aux données permet à chaque test de s’exécuter atomiquement avec une nouvelle instance de base de données :

using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
{
    // Use the db here in the unit test.
}

Chaque méthode de test de la classe DataAccessLayerTest (UnitTests/DataAccessLayerTest.cs) suit un modèle Organisation, Action, Assertion similaire :

  1. Organisation : la base de données est configurée pour le test et/ou le résultat attendu est défini.
  2. Action : le test est exécuté.
  3. Assertion : des assertions sont effectuées pour déterminer si le résultat du test est un succès.

Par exemple, la méthode DeleteMessageAsync est chargée de supprimer un message unique identifié par son Id (src/RazorPagesTestSample/Data/AppDbContext.cs) :

public async virtual Task DeleteMessageAsync(int id)
{
    var message = await Messages.FindAsync(id);

    if (message != null)
    {
        Messages.Remove(message);
        await SaveChangesAsync();
    }
}

Il existe deux tests pour cette méthode. Un test vérifie que la méthode supprime un message lorsque le message est présent dans la base de données. L’autre méthode teste que la base de données ne change pas si le message à supprimer (Id) n’existe pas. Voici la méthode DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound :

[Fact]
public async Task DeleteMessageAsync_MessageIsDeleted_WhenMessageIsFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var seedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(seedMessages);
        await db.SaveChangesAsync();
        var recId = 1;
        var expectedMessages = 
            seedMessages.Where(message => message.Id != recId).ToList();

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

Tout d’abord, la méthode effectue l’étape Organisation, où la préparation de l’étape Action a lieu. Les messages d’amorçage sont obtenus et conservés dans seedMessages. Les messages d’amorçage sont enregistrés dans la base de données. Le message avec un Id de 1 est défini pour suppression. Lorsque la méthode DeleteMessageAsync est exécutée, les messages attendus doivent comprendre tous les messages à l’exception de celui avec un Id de 1. La variable expectedMessages représente ce résultat attendu.

// Arrange
var seedMessages = AppDbContext.GetSeedingMessages();
await db.AddRangeAsync(seedMessages);
await db.SaveChangesAsync();
var recId = 1;
var expectedMessages = 
    seedMessages.Where(message => message.Id != recId).ToList();

La méthode agit : la méthode DeleteMessageAsync est exécutée en passant le recId de 1 :

// Act
await db.DeleteMessageAsync(recId);

Enfin, la méthode obtient Messages à partir du contexte et le compare à expectedMessages pour affirmer que les deux sont égaux :

// Assert
var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

Afin de comparer que les deux List<Message> pour déterminer s’ils sont égaux :

  • Les messages sont classés par Id.
  • Les paires de messages sont comparées sur la propriété Text.

Une méthode de test similaire, DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound, vérifie le résultat de la tentative de suppression d’un message qui n’existe pas. Dans ce cas, les messages attendus dans la base de données doivent être égaux aux messages réels après l’exécution de la méthode DeleteMessageAsync. Le contenu de la base de données ne doit pas être modifié :

[Fact]
public async Task DeleteMessageAsync_NoMessageIsDeleted_WhenMessageIsNotFound()
{
    using (var db = new AppDbContext(Utilities.TestDbContextOptions()))
    {
        // Arrange
        var expectedMessages = AppDbContext.GetSeedingMessages();
        await db.AddRangeAsync(expectedMessages);
        await db.SaveChangesAsync();
        var recId = 4;

        // Act
        await db.DeleteMessageAsync(recId);

        // Assert
        var actualMessages = await db.Messages.AsNoTracking().ToListAsync();
        Assert.Equal(
            expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
            actualMessages.OrderBy(m => m.Id).Select(m => m.Text));
    }
}

Tests unitaires des méthodes de modèle de page

Un autre ensemble de tests unitaires est responsable des tests des méthodes de modèle de page. Dans l’application de message, les modèles de page Index se trouvent dans la classe IndexModel dans src/RazorPagesTestSample/Pages/Index.cshtml.cs.

Méthode de modèle de page Fonction
OnGetAsync Obtient les messages de la couche d’accès aux données pour l’interface utilisateur à l’aide de la méthode GetMessagesAsync.
OnPostAddMessageAsync Si ModelState est valide, appelle AddMessageAsync pour ajouter un message à la base de données.
OnPostDeleteAllMessagesAsync Appelle DeleteAllMessagesAsync pour supprimer tous les messages de la base de données.
OnPostDeleteMessageAsync Exécute DeleteMessageAsync pour supprimer un message avec la valeur Id spécifiée.
OnPostAnalyzeMessagesAsync Si un ou plusieurs messages se trouvent dans la base de données, calcule le nombre moyen de mots par message.

Les méthodes de modèle de page sont testées à l’aide de sept tests dans la classe IndexPageTests (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs). Les tests utilisent le modèle Organisation, Action, Assertion familier. Ces tests visent à :

  • Déterminer si les méthodes suivent le comportement correct lorsque ModelState n’est pas valide.
  • Confirmer que les méthodes produisent le bon IActionResult.
  • Vérifier que les affectations de valeurs de propriété sont effectuées correctement.

Ce groupe de tests simule souvent des méthodes de la couche d’accès aux données pour produire les données attendues pour l’étape Action où une méthode de modèle de page est exécutée. Par exemple, la méthode GetMessagesAsync de AppDbContext est simulée pour produire une sortie. Lorsqu’une méthode de modèle de page exécute cette méthode, la simulation retourne le résultat. Les données ne proviennent pas de la base de données. Cela crée des conditions de test prévisibles et fiables pour l’utilisation de la couche d’accès aux données dans les tests de modèle de page.

Le test OnGetAsync_PopulatesThePageModel_WithAListOfMessages montre comment la méthode GetMessagesAsync est simulée pour le modèle de page :

var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
var expectedMessages = AppDbContext.GetSeedingMessages();
mockAppDbContext.Setup(
    db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
var pageModel = new IndexModel(mockAppDbContext.Object);

Lorsque la méthode OnGetAsync est exécutée à l’étape Action, elle appelle la méthode GetMessagesAsync du modèle de page.

Étape Action du test unitaire (tests/RazorPagesTestSample.Tests/UnitTests/IndexPageTests.cs) :

// Act
await pageModel.OnGetAsync();

Méthode OnGetAsync du modèle de page IndexPage (src/RazorPagesTestSample/Pages/Index.cshtml.cs) :

public async Task OnGetAsync()
{
    Messages = await _db.GetMessagesAsync();
}

La méthode GetMessagesAsync dans la couche d’accès aux données ne retourne pas le résultat de cet appel de méthode. La version simulée de la méthode retourne le résultat.

Dans l’étape Assert, les messages réels (actualMessages) sont attribués à partir de la propriété Messages du modèle de page. Une vérification de type est également effectuée lorsque les messages sont affectés. Les messages attendus et réels sont comparés sur leurs propriétés Text. Le test affirme que les deux instances List<Message> contiennent les mêmes messages.

// Assert
var actualMessages = Assert.IsAssignableFrom<List<Message>>(pageModel.Messages);
Assert.Equal(
    expectedMessages.OrderBy(m => m.Id).Select(m => m.Text), 
    actualMessages.OrderBy(m => m.Id).Select(m => m.Text));

D’autres tests de ce groupe créent des objets de modèle de page qui incluent DefaultHttpContext, ModelStateDictionary, un ActionContext pour établir PageContext, un ViewDataDictionary et un PageContext. Ceux-ci sont utiles pour effectuer les tests. Par exemple, l’application de message établit une erreur ModelState avec AddModelError pour vérifier qu’un PageResult valide est retourné lorsque OnPostAddMessageAsync est exécuté :

[Fact]
public async Task OnPostAddMessageAsync_ReturnsAPageResult_WhenModelStateIsInvalid()
{
    // Arrange
    var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>()
        .UseInMemoryDatabase("InMemoryDb");
    var mockAppDbContext = new Mock<AppDbContext>(optionsBuilder.Options);
    var expectedMessages = AppDbContext.GetSeedingMessages();
    mockAppDbContext.Setup(db => db.GetMessagesAsync()).Returns(Task.FromResult(expectedMessages));
    var httpContext = new DefaultHttpContext();
    var modelState = new ModelStateDictionary();
    var actionContext = new ActionContext(httpContext, new RouteData(), new PageActionDescriptor(), modelState);
    var modelMetadataProvider = new EmptyModelMetadataProvider();
    var viewData = new ViewDataDictionary(modelMetadataProvider, modelState);
    var tempData = new TempDataDictionary(httpContext, Mock.Of<ITempDataProvider>());
    var pageContext = new PageContext(actionContext)
    {
        ViewData = viewData
    };
    var pageModel = new IndexModel(mockAppDbContext.Object)
    {
        PageContext = pageContext,
        TempData = tempData,
        Url = new UrlHelper(actionContext)
    };
    pageModel.ModelState.AddModelError("Message.Text", "The Text field is required.");

    // Act
    var result = await pageModel.OnPostAddMessageAsync();

    // Assert
    Assert.IsType<PageResult>(result);
}

Ressources supplémentaires