Tests d’intégration dans ASP.NET Core

Par Jos van der Til, Martin Costello et Javier Calvarro Nelson.

Les tests d’intégration garantissent que les composants d’une application fonctionnent correctement à un niveau qui inclut l’infrastructure de prise en charge de l’application, comme la base de données, le système de fichiers et le réseau. ASP.NET Core prend en charge les tests d’intégration à l’aide d’une infrastructure de tests unitaires avec un hôte web de test et un serveur de test en mémoire.

Cet article suppose une connaissance élémentaire des tests unitaires. Si vous ne connaissez pas les concepts de test, consultez l’article Tests unitaires dans .NET Core et .NET Standard et son contenu lié.

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

L’exemple d’application est une application Pages Razor et suppose une compréhension de base de Pages Razor. Si vous n’êtes pas familiarisé avec Pages Razor, consultez les articles suivants :

Pour tester les SPA, nous vous recommandons d’utiliser un outil tel que Playwright pour .NET, qui peut automatiser un navigateur.

Présentation des tests d’intégration

Les tests d’intégration évaluent les composants d’une application à un niveau plus large que les tests unitaires. Les tests unitaires sont utilisés pour tester des composants logiciels isolés, tels que des méthodes de classe individuelles. Les tests d’intégration confirment que deux ou plusieurs composants d’application fonctionnent ensemble pour produire un résultat attendu, y compris éventuellement chaque composant requis pour traiter entièrement une requête.

Ces tests plus larges sont utilisés pour tester l’infrastructure et la structure complète de l’application, y compris souvent les composants suivants :

  • Base de données
  • Système de fichiers
  • Appliances réseau
  • Pipeline requête-réponse

Les tests unitaires utilisent des composants fabriqués, connus comme des faux ou des objets fictifs, à la place des composants d’infrastructure.

En opposition aux tests unitaires, les tests d’intégration :

  • Utilisent les composants réels que l’application utilise en production.
  • Exigent davantage de code et de traitement des données.
  • Prennent plus de temps pour s’exécuter.

Par conséquent, limitez l’utilisation des tests d’intégration aux scénarios d’infrastructure les plus importants. Si un comportement peut être testé à l’aide d’un test unitaire ou d’un test d’intégration, choisissez le test unitaire.

Dans les discussions sur les tests d’intégration, le projet testé est fréquemment appelé système testé, ou « ST » pour faire court. « ST » est utilisé tout au long de cet article pour faire référence à l’application ASP.NET Core testée.

N’écrivez pas de tests d’intégration pour chaque permutation des données et de l’accès aux fichiers avec des bases de données et des systèmes de fichiers. Quel que soit le nombre d’emplacements d’une application qui interagissent avec des bases de données et des systèmes de fichiers, un ensemble ciblé de tests d’intégration de lecture, d’écriture, de mise à jour et de suppression est généralement capable de tester de manière adéquate les composants de base de données et de système de fichiers. Utilisez des tests unitaires pour les tests de routine de la logique de méthode qui interagissent avec ces composants. Dans les tests unitaires, l’utilisation d’infrastructures fausses ou fictives entraîne une exécution des tests plus rapide.

tests d’intégration ASP.NET Core

Les tests d’intégration dans ASP.NET Core nécessitent les éléments suivants :

  • Un projet de test est utilisé pour contenir et exécuter les tests. Le projet de test a une référence au ST.
  • Le projet de test crée un hôte web de test pour le ST et utilise un client de serveur de test pour gérer les demandes et les réponses avec le ST.
  • Un exécuteur de test est utilisé pour exécuter les tests et livrer les résultats des tests.

Les tests d’intégration suivent une séquence d’événements qui inclut les étapes de test habituelles Arrange, Act et Assert :

  1. L’hôte web du ST est configuré.
  2. Un client de serveur de test est créé pour envoyer des requêtes à l’application.
  3. L’étape de test Arrange est exécutée : l’application de test prépare une requête.
  4. L’étape de test Act est exécutée : le client envoie la requête et reçoit la réponse.
  5. L’étape de test Assert est exécutée : la réponse réelle est validée en tant que Réussite ou Échec en fonction d’une réponse attendue.
  6. Le processus se poursuit jusqu’à ce que tous les tests soient exécutés.
  7. Les résultats des tests sont livrés.

En règle générale, l’hôte web de test est configuré différemment de l’hôte web normal de l’application pour les séries de tests. Par exemple, une base de données différente ou des paramètres d’application différents peuvent être utilisés pour les tests.

Les composants d’infrastructure, tels que l’hôte web de test et le serveur de test en mémoire (TestServer), sont fournis ou gérés par le package Microsoft.AspNetCore.Mvc.Testing . L’utilisation de ce package simplifie la création et l’exécution des tests.

Le package Microsoft.AspNetCore.Mvc.Testing gère les tâches suivantes :

  • Copie le fichier de dépendances (.deps) du ST dans le répertoire du projet de test bin.
  • Définit la racine du contenu sur la racine du projet du ST afin que soient trouvés les pages/vues et fichiers statiques quand les tests sont exécutés.
  • Il fournit la classe WebApplicationFactory afin de simplifier l’amorçage de l’application testée avec TestServer.

La documentation sur les tests unitaires décrit comment configurer un projet de test et un exécuteur de tests, ainsi que des instructions détaillées sur l’exécution des tests et des recommandations pour nommer les tests et les classes de test.

Séparez les tests unitaires des tests d’intégration dans différents projets. Séparation des tests :

  • Permet de s’assurer que les composants de test d’infrastructure ne sont pas accidentellement inclus dans les tests unitaires.
  • Permet de contrôler quel ensemble de tests sont exécutés.

Il n’existe pratiquement aucune différence entre la configuration des tests des applications Pages Razor et des applications MVC. La seule différence réside dans la façon dont les tests sont nommés. Dans une application Pages Razor, les tests des points de terminaison de page sont généralement nommés d’après la classe de modèle de page (par exemple, IndexPageTests pour tester l’intégration des composants pour la page Index). Dans une application MVC, les tests sont généralement organisés par classes de contrôleur et nommés d’après les contrôleurs qu’ils testent (par exemple, HomeControllerTests pour tester l’intégration des composants pour le Home contrôleur).

Conditions préalables pour tester l’application

Le projet de test doit :

Ces prérequis sont visibles dans l’échantillon d’application. Examinez le fichier tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. L’échantillon d’application utilise l’infrastructure de test xUnit et la bibliothèque d’analyseur AngleSharp, de sorte que l’échantillon d’application référence également :

Dans les applications qui utilisent xunit.runner.visualstudio la version 2.4.2 ou ultérieure, le projet de test doit référencer le package Microsoft.NET.Test.Sdk.

Entity Framework Core est également utilisé dans les tests. Consultez le fichier projet dans GitHub.

Environnement de ST

Si l’environnement de ST n’est pas paramétré, l’environnement est défini par défaut sur Développement.

Tests de base avec webApplicationFactory par défaut

Exposez la classe Program implicitement définie au projet de test en effectuant l’une des opérations suivantes :

  • Exposez les types internes de l’application web au projet de test. Cette opération peut être effectuée dans le fichier du projet de ST (.csproj) :

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Rendez la classe Program publique à l’aide d’une déclaration de classe partielle :

    var builder = WebApplication.CreateBuilder(args);
    // ... Configure services, routes, etc.
    app.Run();
    + public partial class Program { }
    

    L’échantillon d’application utilise l’approche de classe partielle Program.

WebApplicationFactory<TEntryPoint> est utilisé pour créer un TestServer pour les tests d’intégration. TEntryPoint est la classe de point d’entrée du ST, généralement Program.cs.

Les classes de test implémentent une interface de fixture de classe (IClassFixture) pour indiquer que la classe contient des tests et fournir des instances d’objets partagés entre les tests de la classe .

La classe de test suivante, BasicTests, utilise WebApplicationFactory pour démarrer le ST et fournir un HttpClient à une méthode de test, Get_EndpointsReturnSuccessAndCorrectContentType. La méthode vérifie que le code d’état de la réponse a réussi (200-299) et que l’en-tête Content-Type est text/html; charset=utf-8 sur plusieurs pages d’application.

CreateClient() crée une instance de HttpClient qui suit automatiquement les redirections et gère cookie.

public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

Par défaut, les éléments non essentiels cookiene sont pas conservés entre les requêtes lorsque la politique de consentement du Règlement général sur la protection des données est activée. Pour conserver les éléments non essentiels cookie, tels que ceux utilisés par le fournisseur TempData, marquez-les comme essentiels dans vos tests. Pour obtenir des instructions comment identifier un cookie comme essentiel, consultez Essentiel cookie.

AngleSharp vs Application Parts pour les contrôles antifalsification

Cet article utilise l’analyseur AngleSharp pour gérer les vérifications antifalsification en chargeant des pages et en analysant le code HTML. Pour tester les points de terminaison des vues contrôleur et Pages Razor à un niveau inférieur, sans vous soucier de la façon dont ils s’affichent dans le navigateur, pensez à utiliser Application Parts. L’approche des composants d’application injecte un contrôleur ou une page Razor dans l’application qui peut être utilisé pour effectuer JSdes requêtes ON afin d’obtenir les valeurs requises. Pour plus d’informations, consultez le blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts et le référentiel GitHub associé par Martin Costello.

Personnaliser WebApplicationFactory

La configuration de l’hôte web peut être créée indépendamment des classes de test en héritant de WebApplicationFactory<TEntryPoint> pour créer une ou plusieurs fabriques personnalisées :

  1. Hériter de WebApplicationFactory et remplacer la méthode ConfigureWebHost. IWebHostBuilder autorise la configuration de la collection de services avec IWebHostBuilder.ConfigureServices

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    

    L’amorçage de base de données dans l’échantillon d’application est effectué par la méthode InitializeDbForTests. La méthode est décrite dans la section Échantillon de tests d’intégration : organisation de test d’application.

    Le contexte de base de données du ST est inscrit dans Program.cs. Le rappel de l’application de test builder.ConfigureServices est exécuté après l’exécution du code de l’application Program.cs. Pour utiliser une base de données différente pour les tests de la base de données de l’application, le contexte de base de données de l’application doit être remplacé dans builder.ConfigureServices.

    L’échantillon d’application trouve le descripteur de service pour le contexte de base de données et utilise le descripteur pour supprimer l’inscription du service. La fabrique ajoute ensuite un nouveau ApplicationDbContext qui utilise une base de données en mémoire pour les tests.

    Pour vous connecter à une autre base de données, modifiez le DbConnection. Pour utiliser une base de données de test SQL Server :

  1. Utilisez le CustomWebApplicationFactory personnalisé dans les classes de test. L’exemple suivant utilise la fabrique dans la classe IndexPageTests :

    public class IndexPageTests :
        IClassFixture<CustomWebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<Program>
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<Program> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
        }
    

    Le client de l’échantillon d’application est configuré pour empêcher le HttpClient d’effectuer les redirections suivantes. Comme expliqué plus loin dans la section Authentification fictive, cela permet aux tests de vérifier le résultat de la première réponse de l’application. La première réponse est une redirection dans la plupart de ces tests avec un en-tête Location.

  2. Un test classique utilise le HttpClient et les méthodes d’assistance pour traiter la requête et la réponse :

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

Toute requête POST adressée au ST doit satisfaire les contrôles antifalsification effectués automatiquement par le système antifalsification de protection des données de l’application. Pour organiser la requête POST d’un test, l’application de test doit :

  1. Effectuer une requête pour la page.
  2. Analyser l’antifalsification cookie et demander le jeton de validation à partir de la réponse.
  3. Effectuer la requête POST avec l’antifalsification cookie et le jeton de validation de la requête en place.

Les méthodes d’extension d’assistance SendAsync (Helpers/HttpClientExtensions.cs) et la méthode d’assistance GetDocumentAsync (Helpers/HtmlHelpers.cs) dans l’échantillon d’application utilisent l’analyseur AngleSharp pour gérer les contrôles antifalsification avec les méthodes suivantes :

  • GetDocumentAsync: Reçoit le HttpResponseMessage et retourne un IHtmlDocument. GetDocumentAsync utilise une fabrique qui prépare une réponse virtuelle basée sur l’original HttpResponseMessage. Pour plus d’informations, consultez la documentation AngleSharp.
  • Les méthodes d’extension SendAsync pour le HttpClient composent un HttpRequestMessage et appellent SendAsync(HttpRequestMessage) pour envoyer des demandes au ST. Les surcharges pour SendAsync acceptent le formulaire HTML (IHtmlFormElement) et les éléments suivants :
    • Bouton Envoyer du formulaire (IHtmlElement)
    • Collection de valeurs de formulaire (IEnumerable<KeyValuePair<string, string>>)
    • Bouton Envoyer (IHtmlElement) et valeurs de formulaire (IEnumerable<KeyValuePair<string, string>>)

AngleSharp est une bibliothèque d’analyse tierce utilisée à des fins de démonstration dans cet article et dans l’échantillon d’application. AngleSharp n’est pas pris en charge ou requis pour les tests d’intégration des applications ASP.NET Core. D’autres analyseurs peuvent être utilisés, tels que le Html Agility Pack (HAP). Une autre approche consiste à écrire du code pour gérer directement l’antifalsification et le jeton de vérification de requête du système antifalsification cookie. Pour plus d’informations, consultez AngleSharp vs Application Parts pour les contrôles antifalsification dans cet article.

Le fournisseur de base de données en mémoire EF-Core peut être utilisé pour des tests limités et de base, mais le fournisseur SQLite est le choix recommandé pour les tests en mémoire.

Consultez Étendre le démarrage avec des filtres de démarrage qui montre comment configurer l’intergiciel à l’aide de IStartupFilter, ce qui est utile lorsqu’un test nécessite un service ou un intergiciel personnalisé.

Personnaliser le client avec WithWebHostBuilder

Quand une configuration supplémentaire est requise dans une méthode de test, WithWebHostBuilder crée un WebApplicationFactory avec un IWebHostBuilder qui est davantage personnalisé par la configuration.

L’exemple de code appelle WithWebHostBuilder pour remplacer les services configurés par des stubs de test. Pour obtenir plus d’informations et pour des exemples d’utilisation, consultez Injecter des services fictifs dans cet article.

La Post_DeleteMessageHandler_ReturnsRedirectToRoot méthode de test de l’échantillon d’application illustre l’utilisation de WithWebHostBuilder. Ce test effectue une suppression d’enregistrement dans la base de données en déclenchant une soumission de formulaire dans le ST.

Étant donné qu’un autre test de la classe IndexPageTests effectue une opération qui supprime tous les enregistrements de la base de données et peut s’exécuter avant la méthode Post_DeleteMessageHandler_ReturnsRedirectToRoot, la base de données est réamorcée dans cette méthode de test pour s’assurer qu’un enregistrement est présent pour que le ST puisse supprimer. Sélectionner le premier bouton de suppression du formulaire messages dans le ST est simulé dans la requête adressée au ST :

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Options du client

Consultez la page WebApplicationFactoryClientOptions pour connaître les valeurs par défaut et les options disponibles lors de la création d’instances HttpClient.

Créez la classe WebApplicationFactoryClientOptions et passez-la à la méthode CreateClient() :

public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

REMARQUE : Pour éviter les avertissements de redirection HTTPS dans les journaux d’activité, lors de l’utilisation d’intergiciel de redirection HTTPS, définissez BaseAddress = new Uri("https://localhost")

Injecter des services fictifs

Les services peuvent être remplacés dans un test avec un appel à ConfigureTestServices sur le générateur d’hôte. Pour étendre les services remplacés au test lui-même, la méthode WithWebHostBuilder est utilisée pour récupérer un générateur d’hôtes. Vous pouvez le voir dans les tests suivants :

L’échantillon de ST inclut un service délimité qui retourne un devis. Le devis est incorporé dans un champ masqué de la page Index lorsque la page Index est demandée.

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Program.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

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

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

Le balisage suivant est généré lors de l’exécution de l’application ST :

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

Pour tester le service et l’injection de devis dans un test d’intégration, un service fictif est injecté dans le ST par le test. Le service fictif remplace le QuoteService de l’application par un service fourni par l’application de test, appelé TestQuoteService :

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

ConfigureTestServices est appelé et le service délimité est inscrit :

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

Le balisage produit pendant l’exécution du test reflète le texte de guillemet fourni par TestQuoteService, ainsi l’assertion aboutit :

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

Authentification fictive

Les tests dans la classe AuthTests vérifient qu’un point de terminaison sécurisé :

  • Redirige un utilisateur non authentifié vers la page de connexion de l’application.
  • Retourne le contenu d’un utilisateur authentifié.

Dans le ST, la page /SecurePage utilise une convention AuthorizePage pour appliquer un AuthorizeFilter à la page. Pour plus d’informations sur les conventions, consultez Conventions des autorisations de Pages Razor.

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

Dans le test Get_SecurePageRedirectsAnUnauthenticatedUser, un WebApplicationFactoryClientOptions est défini pour interdire les redirections en définissant AllowAutoRedirect sur false :

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

En interdisant au client de suivre la redirection, les vérifications suivantes peuvent être effectuées :

  • Le code d’état retourné par le ST peut être vérifié par rapport au résultat HttpStatusCode.Redirect attendu, et non par rapport au code d’état final après la redirection vers la page de connexion, qui serait HttpStatusCode.OK.
  • La valeur d’en-tête Location dans les en-têtes de réponse est vérifiée pour confirmer qu’elle commence par http://localhost/Identity/Account/Login, et non par la réponse de la page de connexion finale, où l’en-tête Location ne serait pas présent.

L’application de test peut simuler un AuthenticationHandler<TOptions> dans ConfigureTestServices afin de tester des aspects de l’authentification et de l’autorisation. Un scénario minimal retourne un AuthenticateResult.Success :

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

TestAuthHandler est appelé pour authentifier un utilisateur lorsque le schéma d’authentification est défini sur TestScheme quand AddAuthentication est inscrit pour ConfigureTestServices. Il est important que le schéma TestScheme corresponde à celui attendu par votre application. Sinon, l’authentification ne fonctionnera pas.

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(defaultScheme: "TestScheme")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Pour plus d’informations sur WebApplicationFactoryClientOptions, consultez la section Options du client.

Tests de base pour l’intergiciel d’authentification

Consultez ce référentiel GitHub pour les tests de base de l’intergiciel d’authentification. Il contient un serveur de test spécifique au scénario de test.

Définir l’environnement

Définissez l’environnement dans la fabrique d’application personnalisée :

public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbContextOptions<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}

Comment l’infrastructure de test déduit le chemin racine du contenu de l’application

Le constructeur WebApplicationFactory déduit le chemin racine du contenu de l’application en recherchant un WebApplicationFactoryContentRootAttribute sur l’assembly contenant les tests d’intégration avec une clé égale à TEntryPointl’assembly System.Reflection.Assembly.FullName. Si un attribut avec la clé correcte est introuvable, WebApplicationFactory revient à rechercher un fichier de solution (.sln) et ajoute le nom de l’assembly TEntryPoint au répertoire de la solution. Le répertoire racine de l’application (le chemin racine du contenu) est utilisé pour découvrir les vues et les fichiers de contenu.

Désactiver le cliché instantané

Le cliché instantané entraîne l’exécution des tests dans un répertoire différent du répertoire de sortie. Si vos tests reposent sur le chargement de fichiers relatifs à Assembly.Location et que vous rencontrez des problèmes, vous devrez peut-être désactiver le cliché instantané.

Pour désactiver le cliché instantané lors de l’utilisation de xUnit, créez un fichier xunit.runner.json dans le répertoire de votre projet de test, avec le paramètre de configuration correct :

{
  "shadowCopy": false
}

Élimination des objets

Une fois les tests de l’implémentation IClassFixture exécutés, TestServer et HttpClient sont supprimés lorsque xUnit supprime le WebApplicationFactory. Si les objets instanciés par le développeur doivent être éliminés, éliminez-les dans l’implémentation IClassFixture. Pour plus d’informations, consultez Implémentation d’une méthode Dispose.

Exemple de tests d’intégration

L’échantillon d’application est composé de deux applications :

App Répertoire du projet Description
Application de message (ST) src/RazorPagesProject Permet à un utilisateur d’ajouter, de supprimer un message, de tout supprimer et d’analyser les messages.
Tester une application tests/RazorPagesProject.Tests Utilisé pour tester l’intégration du ST.

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 répertoire tests/RazorPagesProject.Tests :

dotnet test

Organisation d’application de message (ST)

Le ST est un système de messages Pages Razor 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 (mots moyens 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).
  • Si la base de données est vide au démarrage de l’application, la banque de messages est initialisée avec trois messages.
  • L’application inclut un /SecurePage qui n’est accessible qu’à un utilisateur authentifié.

†L’article 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’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 (l’échantillon implémente le modèle de référentiel).

Tester l’organisation de l’application

L’application de test est une application console à l’intérieur du répertoire tests/RazorPagesProject.Tests.

Répertoire de l’application de test Description
AuthTests Contient des méthodes de test pour :
  • Accès à une page sécurisée par un utilisateur non authentifié.
  • Accès à une page sécurisée par un utilisateur authentifié avec un AuthenticationHandler<TOptions> fictif.
  • Obtention d’un profil utilisateur GitHub et vérification de la connexion utilisateur du profil.
BasicTests Contient une méthode de test pour le routage et le type de contenu.
IntegrationTests Contient les tests d’intégration pour la page Index à l’aide de la classe personnalisée WebApplicationFactory.
Helpers/Utilities
  • Utilities.cs contient la méthode InitializeDbForTests utilisée pour amorcer la base de données avec des données de test.
  • HtmlHelpers.cs fournit une méthode pour renvoyer un IHtmlDocument AngleSharp à utiliser avec les méthodes de test.
  • HttpClientExtensions.cs fournissent des surcharges pour SendAsync pour envoyer des demandes au ST.

Le framework de test est xUnit. Les tests d’intégration sont effectués à l’aide de Microsoft.AspNetCore.TestHost, qui inclut le TestServer. Étant donné que le package Microsoft.AspNetCore.Mvc.Testing est utilisé pour configurer l’hôte de test et le serveur de test, les packages TestHost et TestServer ne nécessitent pas de références de package directes dans le fichier projet de l’application de test ou la configuration du développeur dans l’application de test.

Les tests d’intégration nécessitent généralement un petit jeu de données dans la base de données avant l’exécution du test. Par exemple, un test de suppression appelle une suppression d’enregistrement de base de données, de sorte que la base de données doit avoir au moins un enregistrement pour que la requête de suppression aboutisse.

L’échantillon d’application amorcera la base de données avec trois messages dans Utilities.cs que les tests peuvent utiliser lorsqu’ils s’exécutent :

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

Le contexte de base de données du ST est inscrit dans Program.cs. Le rappel de l’application de test builder.ConfigureServices est exécuté après l’exécution du code de l’application Program.cs. Pour utiliser une autre base de données pour les tests, le contexte de base de données de l’application doit être remplacé dans builder.ConfigureServices. Pour plus d’informations, consultez la section Personnaliser WebApplicationFactory .

Ressources supplémentaires

Cette rubrique suppose une compréhension de base des tests unitaires. Si vous ne connaissez pas les concepts de test, consultez la rubrique Tests unitaires dans .NET Core et .NET Standard et son contenu lié.

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

L’exemple d’application est une application Pages Razor et suppose une compréhension de base de Pages Razor. Si vous ne connaissez pas les Pages Razor, consultez les rubriques suivantes :

Remarque

Pour tester les SPA, nous vous recommandons d’utiliser un outil tel que Playwright for .NET, qui peut automatiser un navigateur.

Présentation des tests d’intégration

Les tests d’intégration évaluent les composants d’une application à un niveau plus large que les tests unitaires. Les tests unitaires sont utilisés pour tester des composants logiciels isolés, tels que des méthodes de classe individuelles. Les tests d’intégration confirment que deux ou plusieurs composants d’application fonctionnent ensemble pour produire un résultat attendu, y compris éventuellement chaque composant requis pour traiter entièrement une requête.

Ces tests plus larges sont utilisés pour tester l’infrastructure et la structure complète de l’application, y compris souvent les composants suivants :

  • Base de données
  • Système de fichiers
  • Appliances réseau
  • Pipeline requête-réponse

Les tests unitaires utilisent des composants fabriqués, connus comme des faux ou des objets fictifs, à la place des composants d’infrastructure.

En opposition aux tests unitaires, les tests d’intégration :

  • Utilisent les composants réels que l’application utilise en production.
  • Exigent davantage de code et de traitement des données.
  • Prennent plus de temps pour s’exécuter.

Par conséquent, limitez l’utilisation des tests d’intégration aux scénarios d’infrastructure les plus importants. Si un comportement peut être testé à l’aide d’un test unitaire ou d’un test d’intégration, choisissez le test unitaire.

Dans les discussions sur les tests d’intégration, le projet testé est fréquemment appelé système testé, ou « ST » pour faire court. « ST » est utilisé tout au long de cet article pour faire référence à l’application ASP.NET Core testée.

N’écrivez pas de tests d’intégration pour chaque permutation des données et de l’accès aux fichiers avec des bases de données et des systèmes de fichiers. Quel que soit le nombre d’emplacements d’une application qui interagissent avec des bases de données et des systèmes de fichiers, un ensemble ciblé de tests d’intégration de lecture, d’écriture, de mise à jour et de suppression est généralement capable de tester de manière adéquate les composants de base de données et de système de fichiers. Utilisez des tests unitaires pour les tests de routine de la logique de méthode qui interagissent avec ces composants. Dans les tests unitaires, l’utilisation d’infrastructures fausses ou fictives entraîne une exécution des tests plus rapide.

tests d’intégration ASP.NET Core

Les tests d’intégration dans ASP.NET Core nécessitent les éléments suivants :

  • Un projet de test est utilisé pour contenir et exécuter les tests. Le projet de test a une référence au ST.
  • Le projet de test crée un hôte web de test pour le ST et utilise un client de serveur de test pour gérer les demandes et les réponses avec le ST.
  • Un exécuteur de test est utilisé pour exécuter les tests et livrer les résultats des tests.

Les tests d’intégration suivent une séquence d’événements qui inclut les étapes de test habituelles Arrange, Act et Assert :

  1. L’hôte web du ST est configuré.
  2. Un client de serveur de test est créé pour envoyer des requêtes à l’application.
  3. L’étape de test Arrange est exécutée : l’application de test prépare une requête.
  4. L’étape de test Act est exécutée : le client envoie la requête et reçoit la réponse.
  5. L’étape de test Assert est exécutée : la réponse réelle est validée en tant que Réussite ou Échec en fonction d’une réponse attendue.
  6. Le processus se poursuit jusqu’à ce que tous les tests soient exécutés.
  7. Les résultats des tests sont livrés.

En règle générale, l’hôte web de test est configuré différemment de l’hôte web normal de l’application pour les séries de tests. Par exemple, une base de données différente ou des paramètres d’application différents peuvent être utilisés pour les tests.

Les composants d’infrastructure, tels que l’hôte web de test et le serveur de test en mémoire (TestServer), sont fournis ou gérés par le package Microsoft.AspNetCore.Mvc.Testing . L’utilisation de ce package simplifie la création et l’exécution des tests.

Le package Microsoft.AspNetCore.Mvc.Testing gère les tâches suivantes :

  • Copie le fichier de dépendances (.deps) du ST dans le répertoire du projet de test bin.
  • Définit la racine du contenu sur la racine du projet du ST afin que soient trouvés les pages/vues et fichiers statiques quand les tests sont exécutés.
  • Il fournit la classe WebApplicationFactory afin de simplifier l’amorçage de l’application testée avec TestServer.

La documentation sur les tests unitaires décrit comment configurer un projet de test et un exécuteur de tests, ainsi que des instructions détaillées sur l’exécution des tests et des recommandations pour nommer les tests et les classes de test.

Séparez les tests unitaires des tests d’intégration dans différents projets. Séparation des tests :

  • Permet de s’assurer que les composants de test d’infrastructure ne sont pas accidentellement inclus dans les tests unitaires.
  • Permet de contrôler quel ensemble de tests sont exécutés.

Il n’existe pratiquement aucune différence entre la configuration des tests des applications Pages Razor et des applications MVC. La seule différence réside dans la façon dont les tests sont nommés. Dans une application Pages Razor, les tests des points de terminaison de page sont généralement nommés d’après la classe de modèle de page (par exemple, IndexPageTests pour tester l’intégration des composants pour la page Index). Dans une application MVC, les tests sont généralement organisés par classes de contrôleur et nommés d’après les contrôleurs qu’ils testent (par exemple, HomeControllerTests pour tester l’intégration des composants pour le Home contrôleur).

Conditions préalables pour tester l’application

Le projet de test doit :

Ces prérequis sont visibles dans l’échantillon d’application. Examinez le fichier tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. L’échantillon d’application utilise l’infrastructure de test xUnit et la bibliothèque d’analyseur AngleSharp, de sorte que l’échantillon d’application référence également :

Dans les applications qui utilisent xunit.runner.visualstudio la version 2.4.2 ou ultérieure, le projet de test doit référencer le package Microsoft.NET.Test.Sdk.

Entity Framework Core est également utilisé dans les tests. L’application référence :

Environnement de ST

Si l’environnement de ST n’est pas paramétré, l’environnement est défini par défaut sur Développement.

Tests de base avec webApplicationFactory par défaut

WebApplicationFactory<TEntryPoint> est utilisé pour créer un TestServer pour les tests d’intégration. TEntryPoint est la classe de point d’entrée du ST, généralement la classe Startup.

Les classes de test implémentent une interface de fixture de classe (IClassFixture) pour indiquer que la classe contient des tests et fournir des instances d’objets partagés entre les tests de la classe .

La classe de test suivante, BasicTests, utilise WebApplicationFactory pour démarrer le ST et fournir un HttpClient à une méthode de test, Get_EndpointsReturnSuccessAndCorrectContentType. La méthode vérifie si le code d’état de la réponse est correct (codes d’état dans la plage 200-299) et si l’en-tête Content-Type concerne text/html; charset=utf-8 pour plusieurs pages d’application.

CreateClient() crée une instance de HttpClient qui suit automatiquement les redirections et gère cookie.

public class BasicTests 
    : IClassFixture<WebApplicationFactory<RazorPagesProject.Startup>>
{
    private readonly WebApplicationFactory<RazorPagesProject.Startup> _factory;

    public BasicTests(WebApplicationFactory<RazorPagesProject.Startup> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

Par défaut, les éléments non essentiels cookie ne sont pas conservés entre les requêtes lorsque la politique de consentement RGPD est activée. Pour conserver les éléments non essentiels cookie, tels que ceux utilisés par le fournisseur TempData, marquez-les comme essentiels dans vos tests. Pour obtenir des instructions comment identifier un cookie comme essentiel, consultez Essentiel cookie.

Personnaliser WebApplicationFactory

La configuration de l’hôte web peut être créée indépendamment des classes de test en héritant de WebApplicationFactory pour créer une ou plusieurs fabriques personnalisées :

  1. Hériter de WebApplicationFactory et remplacer la méthode ConfigureWebHost. IWebHostBuilder autorise la configuration de la collection de services avec ConfigureServices :

    public class CustomWebApplicationFactory<TStartup>
        : WebApplicationFactory<TStartup> where TStartup: class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var descriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(descriptor);
    
                services.AddDbContext<ApplicationDbContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                });
    
                var sp = services.BuildServiceProvider();
    
                using (var scope = sp.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices.GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<CustomWebApplicationFactory<TStartup>>>();
    
                    db.Database.EnsureCreated();
    
                    try
                    {
                        Utilities.InitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding the " +
                            "database with test messages. Error: {Message}", ex.Message);
                    }
                }
            });
        }
    }
    

    L’amorçage de base de données dans l’échantillon d’application est effectué par la méthode InitializeDbForTests. La méthode est décrite dans la section Échantillon de tests d’intégration : organisation de test d’application.

    Le contexte de base de données du ST est inscrit dans sa méthode Startup.ConfigureServices. Le rappel de l’application de test builder.ConfigureServices est exécuté après l’exécution du code de l’application Startup.ConfigureServices. L’ordre d’exécution est un changement cassant pour l’hôte générique avec la publication de ASP.NET Core 3.0. Pour utiliser une base de données différente pour les tests de la base de données de l’application, le contexte de base de données de l’application doit être remplacé dans builder.ConfigureServices.

    Pour les ST qui utilisent toujours l’hôte web, le rappel de l’application de test builder.ConfigureServices est exécuté avant le code du ST Startup.ConfigureServices. Le rappel de l’application de test builder.ConfigureTestServices est exécuté après.

    L’échantillon d’application trouve le descripteur de service pour le contexte de base de données et utilise le descripteur pour supprimer l’inscription du service. Ensuite, la fabrique ajoute un nouveau ApplicationDbContext qui utilise une base de données en mémoire pour les tests.

    Pour vous connecter à une base de données différente de la base de données en mémoire, modifiez l’appel UseInMemoryDatabase pour connecter le contexte à une autre base de données. Pour utiliser une base de données de test SQL Server :

    services.AddDbContext<ApplicationDbContext>((options, context) => 
    {
        context.UseSqlServer(
            Configuration.GetConnectionString("TestingDbConnectionString"));
    });
    
  2. Utilisez le CustomWebApplicationFactory personnalisé dans les classes de test. L’exemple suivant utilise la fabrique dans la classe IndexPageTests :

    public class IndexPageTests : 
        IClassFixture<CustomWebApplicationFactory<RazorPagesProject.Startup>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<RazorPagesProject.Startup> 
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<RazorPagesProject.Startup> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
                {
                    AllowAutoRedirect = false
                });
        }
    

    Le client de l’échantillon d’application est configuré pour empêcher le HttpClient d’effectuer les redirections suivantes. Comme expliqué plus loin dans la section Authentification fictive, cela permet aux tests de vérifier le résultat de la première réponse de l’application. La première réponse est une redirection dans la plupart de ces tests avec un en-tête Location.

  3. Un test classique utilise le HttpClient et les méthodes d’assistance pour traiter la requête et la réponse :

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

Toute requête POST adressée au ST doit satisfaire les contrôles antifalsification effectués automatiquement par le système antifalsification de protection des données de l’application. Pour organiser la requête POST d’un test, l’application de test doit :

  1. Effectuer une requête pour la page.
  2. Analyser l’antifalsification cookie et demander le jeton de validation à partir de la réponse.
  3. Effectuer la requête POST avec l’antifalsification cookie et le jeton de validation de la requête en place.

Les méthodes d’extension d’assistance SendAsync (Helpers/HttpClientExtensions.cs) et la méthode d’assistance GetDocumentAsync (Helpers/HtmlHelpers.cs) dans l’échantillon d’application utilisent l’analyseur AngleSharp pour gérer les contrôles antifalsification avec les méthodes suivantes :

  • GetDocumentAsync: Reçoit le HttpResponseMessage et retourne un IHtmlDocument. GetDocumentAsync utilise une fabrique qui prépare une réponse virtuelle basée sur l’original HttpResponseMessage. Pour plus d’informations, consultez la documentation AngleSharp.
  • Les méthodes d’extension SendAsync pour le HttpClient composent un HttpRequestMessage et appellent SendAsync(HttpRequestMessage) pour envoyer des demandes au ST. Les surcharges pour SendAsync acceptent le formulaire HTML (IHtmlFormElement) et les éléments suivants :
    • Bouton Envoyer du formulaire (IHtmlElement)
    • Collection de valeurs de formulaire (IEnumerable<KeyValuePair<string, string>>)
    • Bouton Envoyer (IHtmlElement) et valeurs de formulaire (IEnumerable<KeyValuePair<string, string>>)

Remarque

AngleSharp est une bibliothèque d’analyse tierce utilisée à des fins de démonstration dans cette rubrique et dans l’échantillon d’application. AngleSharp n’est pas pris en charge ou requis pour les tests d’intégration des applications ASP.NET Core. D’autres analyseurs peuvent être utilisés, tels que le Html Agility Pack (HAP). Une autre approche consiste à écrire du code pour gérer directement l’antifalsification et le jeton de vérification de requête du système antifalsification cookie.

Remarque

Le fournisseur de base de données en mémoire EF-Core peut être utilisé pour des tests limités et de base, mais le fournisseur SQLite est le choix recommandé pour les tests en mémoire.

Personnaliser le client avec WithWebHostBuilder

Quand une configuration supplémentaire est requise dans une méthode de test, WithWebHostBuilder crée un WebApplicationFactory avec un IWebHostBuilder qui est davantage personnalisé par la configuration.

La Post_DeleteMessageHandler_ReturnsRedirectToRoot méthode de test de l’échantillon d’application illustre l’utilisation de WithWebHostBuilder. Ce test effectue une suppression d’enregistrement dans la base de données en déclenchant une soumission de formulaire dans le ST.

Étant donné qu’un autre test de la classe IndexPageTests effectue une opération qui supprime tous les enregistrements de la base de données et peut s’exécuter avant la méthode Post_DeleteMessageHandler_ReturnsRedirectToRoot, la base de données est réamorcée dans cette méthode de test pour s’assurer qu’un enregistrement est présent pour que le ST puisse supprimer. Sélectionner le premier bouton de suppression du formulaire messages dans le ST est simulé dans la requête adressée au ST :

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                var serviceProvider = services.BuildServiceProvider();

                using (var scope = serviceProvider.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices
                        .GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<IndexPageTests>>();

                    try
                    {
                        Utilities.ReinitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding " +
                            "the database with test messages. Error: {Message}", 
                            ex.Message);
                    }
                }
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Options du client

Le tableau suivant montre la valeur par défaut WebApplicationFactoryClientOptions disponible lors de la création d’instances HttpClient.

Option Description Default
AllowAutoRedirect Obtient ou définit, si les instances HttpClient doivent ou non suivre automatiquement les réponses de redirection. true
BaseAddress Obtient ou définit l’adresse de base des instances HttpClient. http://localhost
HandleCookies Obtient ou définit si les instances HttpClient doivent gérer cookie. true
MaxAutomaticRedirections Obtient ou définit le nombre maximal de réponses de redirection que les instances HttpClient doivent suivre. 7

Créez la classe WebApplicationFactoryClientOptions et passez-la à la méthode CreateClient() (les valeurs par défaut sont indiquées dans l’exemple de code) :

// Default client option values are shown
var clientOptions = new WebApplicationFactoryClientOptions();
clientOptions.AllowAutoRedirect = true;
clientOptions.BaseAddress = new Uri("http://localhost");
clientOptions.HandleCookies = true;
clientOptions.MaxAutomaticRedirections = 7;

_client = _factory.CreateClient(clientOptions);

Injecter des services fictifs

Les services peuvent être remplacés dans un test avec un appel à ConfigureTestServices sur le générateur d’hôte. Pour injecter des services fictifs, le ST doit avoir une classe Startup avec une méthode Startup.ConfigureServices.

L’échantillon de ST inclut un service délimité qui retourne un devis. Le devis est incorporé dans un champ masqué de la page Index lorsque la page Index est demandée.

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Startup.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

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

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

Le balisage suivant est généré lors de l’exécution de l’application ST :

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

Pour tester le service et l’injection de devis dans un test d’intégration, un service fictif est injecté dans le ST par le test. Le service fictif remplace le QuoteService de l’application par un service fourni par l’application de test, appelé TestQuoteService :

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

ConfigureTestServices est appelé et le service délimité est inscrit :

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

Le balisage produit pendant l’exécution du test reflète le texte de guillemet fourni par TestQuoteService, ainsi l’assertion aboutit :

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

Authentification fictive

Les tests dans la classe AuthTests vérifient qu’un point de terminaison sécurisé :

  • Redirige un utilisateur non authentifié vers la page de connexion de l’application.
  • Retourne le contenu d’un utilisateur authentifié.

Dans le ST, la page /SecurePage utilise une convention AuthorizePage pour appliquer un AuthorizeFilter à la page. Pour plus d’informations sur les conventions, consultez Conventions des autorisations de Pages Razor.

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

Dans le test Get_SecurePageRedirectsAnUnauthenticatedUser, un WebApplicationFactoryClientOptions est défini pour interdire les redirections en définissant AllowAutoRedirect sur false :

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login", 
        response.Headers.Location.OriginalString);
}

En interdisant au client de suivre la redirection, les vérifications suivantes peuvent être effectuées :

  • Le code d’état retourné par le ST peut être vérifié par rapport au résultat attendu HttpStatusCode.Redirect, et non par rapport au code d’état final après la redirection vers la page de connexion, qui serait HttpStatusCode.OK.
  • La valeur d’en-tête Location dans les en-têtes de réponse est vérifiée pour confirmer qu’elle commence par http://localhost/Identity/Account/Login, et non par la réponse de la page de connexion finale, où l’en-tête Location ne serait pas présent.

L’application de test peut simuler un AuthenticationHandler<TOptions> dans ConfigureTestServices afin de tester des aspects de l’authentification et de l’autorisation. Un scénario minimal retourne un AuthenticateResult.Success :

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, 
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

TestAuthHandler est appelé pour authentifier un utilisateur lorsque le schéma d’authentification est défini sur Test quand AddAuthentication est inscrit pour ConfigureTestServices. Il est important que le schéma Test corresponde à celui attendu par votre application. Sinon, l’authentification ne fonctionnera pas.

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication("Test")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "Test", options => {});
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization = 
        new AuthenticationHeaderValue("Test");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Pour plus d’informations sur WebApplicationFactoryClientOptions, consultez la section Options du client.

Définir l’environnement

Par défaut, l’environnement hôte et l’environnement d’application du ST sont configurés pour utiliser l’environnement de développement. Pour remplacer l’environnement du ST lors de l’utilisation de IHostBuilder :

  • Définissez la variable d’environnement ASPNETCORE_ENVIRONMENT (par exemple, Staging, Productionou une autre valeur personnalisée, telle que Testing).
  • Remplacez CreateHostBuilder dans l’application de test pour lire les variables d’environnement précédées de ASPNETCORE.
protected override IHostBuilder CreateHostBuilder() =>
    base.CreateHostBuilder()
        .ConfigureHostConfiguration(
            config => config.AddEnvironmentVariables("ASPNETCORE"));

Si le ST utilise l’hôte web (IWebHostBuilder), remplacez CreateWebHostBuilder :

protected override IWebHostBuilder CreateWebHostBuilder() =>
    base.CreateWebHostBuilder().UseEnvironment("Testing");

Comment l’infrastructure de test déduit le chemin racine du contenu de l’application

Le constructeur WebApplicationFactory déduit le chemin racine du contenu de l’application en recherchant un WebApplicationFactoryContentRootAttribute sur l’assembly contenant les tests d’intégration avec une clé égale à TEntryPointl’assembly System.Reflection.Assembly.FullName. Si un attribut avec la clé correcte est introuvable, WebApplicationFactory revient à rechercher un fichier de solution (.sln) et ajoute le nom de l’assembly TEntryPoint au répertoire de la solution. Le répertoire racine de l’application (le chemin racine du contenu) est utilisé pour découvrir les vues et les fichiers de contenu.

Désactiver le cliché instantané

Le cliché instantané entraîne l’exécution des tests dans un répertoire différent du répertoire de sortie. Si vos tests reposent sur le chargement de fichiers relatifs à Assembly.Location et que vous rencontrez des problèmes, vous devrez peut-être désactiver le cliché instantané.

Pour désactiver le cliché instantané lors de l’utilisation de xUnit, créez un fichier xunit.runner.json dans le répertoire de votre projet de test, avec le paramètre de configuration correct :

{
  "shadowCopy": false
}

Élimination des objets

Une fois les tests de l’implémentation IClassFixture exécutés, TestServer et HttpClient sont supprimés lorsque xUnit supprime le WebApplicationFactory. Si les objets instanciés par le développeur doivent être éliminés, éliminez-les dans l’implémentation IClassFixture. Pour plus d’informations, consultez Implémentation d’une méthode Dispose.

Exemple de tests d’intégration

L’échantillon d’application est composé de deux applications :

App Répertoire du projet Description
Application de message (ST) src/RazorPagesProject Permet à un utilisateur d’ajouter, de supprimer un message, de tout supprimer et d’analyser les messages.
Tester une application tests/RazorPagesProject.Tests Utilisé pour tester l’intégration du ST.

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 répertoire tests/RazorPagesProject.Tests :

dotnet test

Organisation d’application de message (ST)

Le ST est un système de messages Pages Razor 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 (mots moyens 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).
  • Si la base de données est vide au démarrage de l’application, la banque de messages est initialisée avec trois messages.
  • L’application inclut un /SecurePage qui n’est accessible qu’à un utilisateur authentifié.

†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’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 (l’échantillon implémente le modèle de référentiel).

Tester l’organisation de l’application

L’application de test est une application console à l’intérieur du répertoire tests/RazorPagesProject.Tests.

Répertoire de l’application de test Description
AuthTests Contient des méthodes de test pour :
  • Accès à une page sécurisée par un utilisateur non authentifié.
  • Accès à une page sécurisée par un utilisateur authentifié avec un AuthenticationHandler<TOptions> fictif.
  • Obtention d’un profil utilisateur GitHub et vérification de la connexion utilisateur du profil.
BasicTests Contient une méthode de test pour le routage et le type de contenu.
IntegrationTests Contient les tests d’intégration pour la page Index à l’aide de la classe personnalisée WebApplicationFactory.
Helpers/Utilities
  • Utilities.cs contient la méthode InitializeDbForTests utilisée pour amorcer la base de données avec des données de test.
  • HtmlHelpers.cs fournit une méthode pour renvoyer un IHtmlDocument AngleSharp à utiliser avec les méthodes de test.
  • HttpClientExtensions.cs fournissent des surcharges pour SendAsync pour envoyer des demandes au ST.

Le framework de test est xUnit. Les tests d’intégration sont effectués à l’aide de Microsoft.AspNetCore.TestHost, qui inclut le TestServer. Étant donné que le package Microsoft.AspNetCore.Mvc.Testing est utilisé pour configurer l’hôte de test et le serveur de test, les packages TestHost et TestServer ne nécessitent pas de références de package directes dans le fichier projet de l’application de test ou la configuration du développeur dans l’application de test.

Les tests d’intégration nécessitent généralement un petit jeu de données dans la base de données avant l’exécution du test. Par exemple, un test de suppression appelle une suppression d’enregistrement de base de données, de sorte que la base de données doit avoir au moins un enregistrement pour que la requête de suppression aboutisse.

L’échantillon d’application amorcera la base de données avec trois messages dans Utilities.cs que les tests peuvent utiliser lorsqu’ils s’exécutent :

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

Le contexte de base de données du ST est inscrit dans sa méthode Startup.ConfigureServices. Le rappel de l’application de test builder.ConfigureServices est exécuté après l’exécution du code de l’application Startup.ConfigureServices. Pour utiliser une autre base de données pour les tests, le contexte de base de données de l’application doit être remplacé dans builder.ConfigureServices. Pour plus d’informations, consultez la section Personnaliser WebApplicationFactory .

Pour les ST qui utilisent toujours l’hôte web, le rappel de l’application de test builder.ConfigureServices est exécuté avant le code du ST Startup.ConfigureServices. Le rappel de l’application de test builder.ConfigureTestServices est exécuté après.

Ressources supplémentaires

Cet article suppose une connaissance élémentaire des tests unitaires. Si vous ne connaissez pas les concepts de test, consultez l’article Tests unitaires dans .NET Core et .NET Standard et son contenu lié.

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

L’exemple d’application est une application Pages Razor et suppose une compréhension de base de Pages Razor. Si vous n’êtes pas familiarisé avec Pages Razor, consultez les articles suivants :

Pour tester les SPA, nous vous recommandons d’utiliser un outil tel que Playwright pour .NET, qui peut automatiser un navigateur.

Présentation des tests d’intégration

Les tests d’intégration évaluent les composants d’une application à un niveau plus large que les tests unitaires. Les tests unitaires sont utilisés pour tester des composants logiciels isolés, tels que des méthodes de classe individuelles. Les tests d’intégration confirment que deux ou plusieurs composants d’application fonctionnent ensemble pour produire un résultat attendu, y compris éventuellement chaque composant requis pour traiter entièrement une requête.

Ces tests plus larges sont utilisés pour tester l’infrastructure et la structure complète de l’application, y compris souvent les composants suivants :

  • Base de données
  • Système de fichiers
  • Appliances réseau
  • Pipeline requête-réponse

Les tests unitaires utilisent des composants fabriqués, connus comme des faux ou des objets fictifs, à la place des composants d’infrastructure.

En opposition aux tests unitaires, les tests d’intégration :

  • Utilisent les composants réels que l’application utilise en production.
  • Exigent davantage de code et de traitement des données.
  • Prennent plus de temps pour s’exécuter.

Par conséquent, limitez l’utilisation des tests d’intégration aux scénarios d’infrastructure les plus importants. Si un comportement peut être testé à l’aide d’un test unitaire ou d’un test d’intégration, choisissez le test unitaire.

Dans les discussions sur les tests d’intégration, le projet testé est fréquemment appelé système testé, ou « ST » pour faire court. « ST » est utilisé tout au long de cet article pour faire référence à l’application ASP.NET Core testée.

N’écrivez pas de tests d’intégration pour chaque permutation des données et de l’accès aux fichiers avec des bases de données et des systèmes de fichiers. Quel que soit le nombre d’emplacements d’une application qui interagissent avec des bases de données et des systèmes de fichiers, un ensemble ciblé de tests d’intégration de lecture, d’écriture, de mise à jour et de suppression est généralement capable de tester de manière adéquate les composants de base de données et de système de fichiers. Utilisez des tests unitaires pour les tests de routine de la logique de méthode qui interagissent avec ces composants. Dans les tests unitaires, l’utilisation d’infrastructures fausses ou fictives entraîne une exécution des tests plus rapide.

tests d’intégration ASP.NET Core

Les tests d’intégration dans ASP.NET Core nécessitent les éléments suivants :

  • Un projet de test est utilisé pour contenir et exécuter les tests. Le projet de test a une référence au ST.
  • Le projet de test crée un hôte web de test pour le ST et utilise un client de serveur de test pour gérer les demandes et les réponses avec le ST.
  • Un exécuteur de test est utilisé pour exécuter les tests et livrer les résultats des tests.

Les tests d’intégration suivent une séquence d’événements qui inclut les étapes de test habituelles Arrange, Act et Assert :

  1. L’hôte web du ST est configuré.
  2. Un client de serveur de test est créé pour envoyer des requêtes à l’application.
  3. L’étape de test Arrange est exécutée : l’application de test prépare une requête.
  4. L’étape de test Act est exécutée : le client envoie la requête et reçoit la réponse.
  5. L’étape de test Assert est exécutée : la réponse réelle est validée en tant que Réussite ou Échec en fonction d’une réponse attendue.
  6. Le processus se poursuit jusqu’à ce que tous les tests soient exécutés.
  7. Les résultats des tests sont livrés.

En règle générale, l’hôte web de test est configuré différemment de l’hôte web normal de l’application pour les séries de tests. Par exemple, une base de données différente ou des paramètres d’application différents peuvent être utilisés pour les tests.

Les composants d’infrastructure, tels que l’hôte web de test et le serveur de test en mémoire (TestServer), sont fournis ou gérés par le package Microsoft.AspNetCore.Mvc.Testing . L’utilisation de ce package simplifie la création et l’exécution des tests.

Le package Microsoft.AspNetCore.Mvc.Testing gère les tâches suivantes :

  • Copie le fichier de dépendances (.deps) du ST dans le répertoire du projet de test bin.
  • Définit la racine du contenu sur la racine du projet du ST afin que soient trouvés les pages/vues et fichiers statiques quand les tests sont exécutés.
  • Il fournit la classe WebApplicationFactory afin de simplifier l’amorçage de l’application testée avec TestServer.

La documentation sur les tests unitaires décrit comment configurer un projet de test et un exécuteur de tests, ainsi que des instructions détaillées sur l’exécution des tests et des recommandations pour nommer les tests et les classes de test.

Séparez les tests unitaires des tests d’intégration dans différents projets. Séparation des tests :

  • Permet de s’assurer que les composants de test d’infrastructure ne sont pas accidentellement inclus dans les tests unitaires.
  • Permet de contrôler quel ensemble de tests sont exécutés.

Il n’existe pratiquement aucune différence entre la configuration des tests des applications Pages Razor et des applications MVC. La seule différence réside dans la façon dont les tests sont nommés. Dans une application Pages Razor, les tests des points de terminaison de page sont généralement nommés d’après la classe de modèle de page (par exemple, IndexPageTests pour tester l’intégration des composants pour la page Index). Dans une application MVC, les tests sont généralement organisés par classes de contrôleur et nommés d’après les contrôleurs qu’ils testent (par exemple, HomeControllerTests pour tester l’intégration des composants pour le Home contrôleur).

Conditions préalables pour tester l’application

Le projet de test doit :

Ces prérequis sont visibles dans l’échantillon d’application. Examinez le fichier tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. L’échantillon d’application utilise l’infrastructure de test xUnit et la bibliothèque d’analyseur AngleSharp, de sorte que l’échantillon d’application référence également :

Dans les applications qui utilisent xunit.runner.visualstudio la version 2.4.2 ou ultérieure, le projet de test doit référencer le package Microsoft.NET.Test.Sdk.

Entity Framework Core est également utilisé dans les tests. Consultez le fichier projet dans GitHub.

Environnement de ST

Si l’environnement de ST n’est pas paramétré, l’environnement est défini par défaut sur Développement.

Tests de base avec webApplicationFactory par défaut

Exposez la classe Program implicitement définie au projet de test en effectuant l’une des opérations suivantes :

  • Exposez les types internes de l’application web au projet de test. Cette opération peut être effectuée dans le fichier du projet de ST (.csproj) :

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Rendez la classe Program publique à l’aide d’une déclaration de classe partielle :

    var builder = WebApplication.CreateBuilder(args);
    // ... Configure services, routes, etc.
    app.Run();
    + public partial class Program { }
    

    L’échantillon d’application utilise l’approche de classe partielle Program.

WebApplicationFactory<TEntryPoint> est utilisé pour créer un TestServer pour les tests d’intégration. TEntryPoint est la classe de point d’entrée du ST, généralement Program.cs.

Les classes de test implémentent une interface de fixture de classe (IClassFixture) pour indiquer que la classe contient des tests et fournir des instances d’objets partagés entre les tests de la classe .

La classe de test suivante, BasicTests, utilise WebApplicationFactory pour démarrer le ST et fournir un HttpClient à une méthode de test, Get_EndpointsReturnSuccessAndCorrectContentType. La méthode vérifie que le code d’état de la réponse a réussi (200-299) et que l’en-tête Content-Type est text/html; charset=utf-8 sur plusieurs pages d’application.

CreateClient() crée une instance de HttpClient qui suit automatiquement les redirections et gère cookie.

public class BasicTests 
    : IClassFixture<WebApplicationFactory<Program>>
{
    private readonly WebApplicationFactory<Program> _factory;

    public BasicTests(WebApplicationFactory<Program> factory)
    {
        _factory = factory;
    }

    [Theory]
    [InlineData("/")]
    [InlineData("/Index")]
    [InlineData("/About")]
    [InlineData("/Privacy")]
    [InlineData("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

        // Act
        var response = await client.GetAsync(url);

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.Equal("text/html; charset=utf-8", 
            response.Content.Headers.ContentType.ToString());
    }
}

Par défaut, les éléments non essentiels cookiene sont pas conservés entre les requêtes lorsque la politique de consentement du Règlement général sur la protection des données est activée. Pour conserver les éléments non essentiels cookie, tels que ceux utilisés par le fournisseur TempData, marquez-les comme essentiels dans vos tests. Pour obtenir des instructions comment identifier un cookie comme essentiel, consultez Essentiel cookie.

AngleSharp vs Application Parts pour les contrôles antifalsification

Cet article utilise l’analyseur AngleSharp pour gérer les vérifications antifalsification en chargeant des pages et en analysant le code HTML. Pour tester les points de terminaison des vues contrôleur et Pages Razor à un niveau inférieur, sans vous soucier de la façon dont ils s’affichent dans le navigateur, pensez à utiliser Application Parts. L’approche des composants d’application injecte un contrôleur ou une page Razor dans l’application qui peut être utilisé pour effectuer JSdes requêtes ON afin d’obtenir les valeurs requises. Pour plus d’informations, consultez le blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts et le référentiel GitHub associé par Martin Costello.

Personnaliser WebApplicationFactory

La configuration de l’hôte web peut être créée indépendamment des classes de test en héritant de WebApplicationFactory<TEntryPoint> pour créer une ou plusieurs fabriques personnalisées :

  1. Hériter de WebApplicationFactory et remplacer la méthode ConfigureWebHost. IWebHostBuilder autorise la configuration de la collection de services avec IWebHostBuilder.ConfigureServices

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbContextOptions<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    

    L’amorçage de base de données dans l’échantillon d’application est effectué par la méthode InitializeDbForTests. La méthode est décrite dans la section Échantillon de tests d’intégration : organisation de test d’application.

    Le contexte de base de données du ST est inscrit dans Program.cs. Le rappel de l’application de test builder.ConfigureServices est exécuté après l’exécution du code de l’application Program.cs. Pour utiliser une base de données différente pour les tests de la base de données de l’application, le contexte de base de données de l’application doit être remplacé dans builder.ConfigureServices.

    L’échantillon d’application trouve le descripteur de service pour le contexte de base de données et utilise le descripteur pour supprimer l’inscription du service. La fabrique ajoute ensuite un nouveau ApplicationDbContext qui utilise une base de données en mémoire pour les tests.

    Pour vous connecter à une autre base de données, modifiez le DbConnection. Pour utiliser une base de données de test SQL Server :

  1. Utilisez le CustomWebApplicationFactory personnalisé dans les classes de test. L’exemple suivant utilise la fabrique dans la classe IndexPageTests :

    public class IndexPageTests :
        IClassFixture<CustomWebApplicationFactory<Program>>
    {
        private readonly HttpClient _client;
        private readonly CustomWebApplicationFactory<Program>
            _factory;
    
        public IndexPageTests(
            CustomWebApplicationFactory<Program> factory)
        {
            _factory = factory;
            _client = factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
        }
    

    Le client de l’échantillon d’application est configuré pour empêcher le HttpClient d’effectuer les redirections suivantes. Comme expliqué plus loin dans la section Authentification fictive, cela permet aux tests de vérifier le résultat de la première réponse de l’application. La première réponse est une redirection dans la plupart de ces tests avec un en-tête Location.

  2. Un test classique utilise le HttpClient et les méthodes d’assistance pour traiter la requête et la réponse :

    [Fact]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
        Assert.Equal("/", response.Headers.Location.OriginalString);
    }
    

Toute requête POST adressée au ST doit satisfaire les contrôles antifalsification effectués automatiquement par le système antifalsification de protection des données de l’application. Pour organiser la requête POST d’un test, l’application de test doit :

  1. Effectuer une requête pour la page.
  2. Analyser l’antifalsification cookie et demander le jeton de validation à partir de la réponse.
  3. Effectuer la requête POST avec l’antifalsification cookie et le jeton de validation de la requête en place.

Les méthodes d’extension d’assistance SendAsync (Helpers/HttpClientExtensions.cs) et la méthode d’assistance GetDocumentAsync (Helpers/HtmlHelpers.cs) dans l’échantillon d’application utilisent l’analyseur AngleSharp pour gérer les contrôles antifalsification avec les méthodes suivantes :

  • GetDocumentAsync: Reçoit le HttpResponseMessage et retourne un IHtmlDocument. GetDocumentAsync utilise une fabrique qui prépare une réponse virtuelle basée sur l’original HttpResponseMessage. Pour plus d’informations, consultez la documentation AngleSharp.
  • Les méthodes d’extension SendAsync pour le HttpClient composent un HttpRequestMessage et appellent SendAsync(HttpRequestMessage) pour envoyer des demandes au ST. Les surcharges pour SendAsync acceptent le formulaire HTML (IHtmlFormElement) et les éléments suivants :
    • Bouton Envoyer du formulaire (IHtmlElement)
    • Collection de valeurs de formulaire (IEnumerable<KeyValuePair<string, string>>)
    • Bouton Envoyer (IHtmlElement) et valeurs de formulaire (IEnumerable<KeyValuePair<string, string>>)

AngleSharp est une bibliothèque d’analyse tierce utilisée à des fins de démonstration dans cet article et dans l’échantillon d’application. AngleSharp n’est pas pris en charge ou requis pour les tests d’intégration des applications ASP.NET Core. D’autres analyseurs peuvent être utilisés, tels que le Html Agility Pack (HAP). Une autre approche consiste à écrire du code pour gérer directement l’antifalsification et le jeton de vérification de requête du système antifalsification cookie. Pour plus d’informations, consultez AngleSharp vs Application Parts pour les contrôles antifalsification dans cet article.

Le fournisseur de base de données en mémoire EF-Core peut être utilisé pour des tests limités et de base, mais le fournisseur SQLite est le choix recommandé pour les tests en mémoire.

Consultez Étendre le démarrage avec des filtres de démarrage qui montre comment configurer l’intergiciel à l’aide de IStartupFilter, ce qui est utile lorsqu’un test nécessite un service ou un intergiciel personnalisé.

Personnaliser le client avec WithWebHostBuilder

Quand une configuration supplémentaire est requise dans une méthode de test, WithWebHostBuilder crée un WebApplicationFactory avec un IWebHostBuilder qui est davantage personnalisé par la configuration.

L’exemple de code appelle WithWebHostBuilder pour remplacer les services configurés par des stubs de test. Pour obtenir plus d’informations et pour des exemples d’utilisation, consultez Injecter des services fictifs dans cet article.

La Post_DeleteMessageHandler_ReturnsRedirectToRoot méthode de test de l’échantillon d’application illustre l’utilisation de WithWebHostBuilder. Ce test effectue une suppression d’enregistrement dans la base de données en déclenchant une soumission de formulaire dans le ST.

Étant donné qu’un autre test de la classe IndexPageTests effectue une opération qui supprime tous les enregistrements de la base de données et peut s’exécuter avant la méthode Post_DeleteMessageHandler_ReturnsRedirectToRoot, la base de données est réamorcée dans cette méthode de test pour s’assurer qu’un enregistrement est présent pour que le ST puisse supprimer. Sélectionner le premier bouton de suppression du formulaire messages dans le ST est simulé dans la requête adressée au ST :

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Options du client

Consultez la page WebApplicationFactoryClientOptions pour connaître les valeurs par défaut et les options disponibles lors de la création d’instances HttpClient.

Créez la classe WebApplicationFactoryClientOptions et passez-la à la méthode CreateClient() :

public class IndexPageTests :
    IClassFixture<CustomWebApplicationFactory<Program>>
{
    private readonly HttpClient _client;
    private readonly CustomWebApplicationFactory<Program>
        _factory;

    public IndexPageTests(
        CustomWebApplicationFactory<Program> factory)
    {
        _factory = factory;
        _client = factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

REMARQUE : Pour éviter les avertissements de redirection HTTPS dans les journaux d’activité, lors de l’utilisation d’intergiciel de redirection HTTPS, définissez BaseAddress = new Uri("https://localhost")

Injecter des services fictifs

Les services peuvent être remplacés dans un test avec un appel à ConfigureTestServices sur le générateur d’hôte. Pour étendre les services remplacés au test lui-même, la méthode WithWebHostBuilder est utilisée pour récupérer un générateur d’hôtes. Vous pouvez le voir dans les tests suivants :

L’échantillon de ST inclut un service délimité qui retourne un devis. Le devis est incorporé dans un champ masqué de la page Index lorsque la page Index est demandée.

Services/IQuoteService.cs:

public interface IQuoteService
{
    Task<string> GenerateQuote();
}

Services/QuoteService.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Dr. Who: Planet of Evil
// https://www.bbc.co.uk/programmes/p00pyrx6
public class QuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Come on, Sarah. We've an appointment in London, " +
            "and we're already 30,000 years late.");
    }
}

Program.cs:

services.AddScoped<IQuoteService, QuoteService>();

Pages/Index.cshtml.cs:

public class IndexModel : PageModel
{
    private readonly ApplicationDbContext _db;
    private readonly IQuoteService _quoteService;

    public IndexModel(ApplicationDbContext db, IQuoteService quoteService)
    {
        _db = db;
        _quoteService = quoteService;
    }

    [BindProperty]
    public Message Message { get; set; }

    public IList<Message> Messages { get; private set; }

    [TempData]
    public string MessageAnalysisResult { get; set; }

    public string Quote { get; private set; }

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

        Quote = await _quoteService.GenerateQuote();
    }

Pages/Index.cs:

<input id="quote" type="hidden" value="@Model.Quote">

Le balisage suivant est généré lors de l’exécution de l’application ST :

<input id="quote" type="hidden" value="Come on, Sarah. We&#x27;ve an appointment in 
    London, and we&#x27;re already 30,000 years late.">

Pour tester le service et l’injection de devis dans un test d’intégration, un service fictif est injecté dans le ST par le test. Le service fictif remplace le QuoteService de l’application par un service fourni par l’application de test, appelé TestQuoteService :

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

ConfigureTestServices est appelé et le service délimité est inscrit :

[Fact]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.Equal("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}

Le balisage produit pendant l’exécution du test reflète le texte de guillemet fourni par TestQuoteService, ainsi l’assertion aboutit :

<input id="quote" type="hidden" value="Something&#x27;s interfering with time, 
    Mr. Scarman, and time is my business.">

Authentification fictive

Les tests dans la classe AuthTests vérifient qu’un point de terminaison sécurisé :

  • Redirige un utilisateur non authentifié vers la page de connexion de l’application.
  • Retourne le contenu d’un utilisateur authentifié.

Dans le ST, la page /SecurePage utilise une convention AuthorizePage pour appliquer un AuthorizeFilter à la page. Pour plus d’informations sur les conventions, consultez Conventions des autorisations de Pages Razor.

services.AddRazorPages(options =>
{
    options.Conventions.AuthorizePage("/SecurePage");
});

Dans le test Get_SecurePageRedirectsAnUnauthenticatedUser, un WebApplicationFactoryClientOptions est défini pour interdire les redirections en définissant AllowAutoRedirect sur false :

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

En interdisant au client de suivre la redirection, les vérifications suivantes peuvent être effectuées :

  • Le code d’état retourné par le ST peut être vérifié par rapport au résultat HttpStatusCode.Redirect attendu, et non par rapport au code d’état final après la redirection vers la page de connexion, qui serait HttpStatusCode.OK.
  • La valeur d’en-tête Location dans les en-têtes de réponse est vérifiée pour confirmer qu’elle commence par http://localhost/Identity/Account/Login, et non par la réponse de la page de connexion finale, où l’en-tête Location ne serait pas présent.

L’application de test peut simuler un AuthenticationHandler<TOptions> dans ConfigureTestServices afin de tester des aspects de l’authentification et de l’autorisation. Un scénario minimal retourne un AuthenticateResult.Success :

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

TestAuthHandler est appelé pour authentifier un utilisateur lorsque le schéma d’authentification est défini sur TestScheme quand AddAuthentication est inscrit pour ConfigureTestServices. Il est important que le schéma TestScheme corresponde à celui attendu par votre application. Sinon, l’authentification ne fonctionnera pas.

[Fact]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddAuthentication(defaultScheme: "TestScheme")
                    .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                        "TestScheme", options => { });
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

    //Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}

Pour plus d’informations sur WebApplicationFactoryClientOptions, consultez la section Options du client.

Tests de base pour l’intergiciel d’authentification

Consultez ce référentiel GitHub pour les tests de base de l’intergiciel d’authentification. Il contient un serveur de test spécifique au scénario de test.

Définir l’environnement

Définissez l’environnement dans la fabrique d’application personnalisée :

public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbContextOptions<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}

Comment l’infrastructure de test déduit le chemin racine du contenu de l’application

Le constructeur WebApplicationFactory déduit le chemin racine du contenu de l’application en recherchant un WebApplicationFactoryContentRootAttribute sur l’assembly contenant les tests d’intégration avec une clé égale à TEntryPointl’assembly System.Reflection.Assembly.FullName. Si un attribut avec la clé correcte est introuvable, WebApplicationFactory revient à rechercher un fichier de solution (.sln) et ajoute le nom de l’assembly TEntryPoint au répertoire de la solution. Le répertoire racine de l’application (le chemin racine du contenu) est utilisé pour découvrir les vues et les fichiers de contenu.

Désactiver le cliché instantané

Le cliché instantané entraîne l’exécution des tests dans un répertoire différent du répertoire de sortie. Si vos tests reposent sur le chargement de fichiers relatifs à Assembly.Location et que vous rencontrez des problèmes, vous devrez peut-être désactiver le cliché instantané.

Pour désactiver le cliché instantané lors de l’utilisation de xUnit, créez un fichier xunit.runner.json dans le répertoire de votre projet de test, avec le paramètre de configuration correct :

{
  "shadowCopy": false
}

Élimination des objets

Une fois les tests de l’implémentation IClassFixture exécutés, TestServer et HttpClient sont supprimés lorsque xUnit supprime le WebApplicationFactory. Si les objets instanciés par le développeur doivent être éliminés, éliminez-les dans l’implémentation IClassFixture. Pour plus d’informations, consultez Implémentation d’une méthode Dispose.

Exemple de tests d’intégration

L’échantillon d’application est composé de deux applications :

App Répertoire du projet Description
Application de message (ST) src/RazorPagesProject Permet à un utilisateur d’ajouter, de supprimer un message, de tout supprimer et d’analyser les messages.
Tester une application tests/RazorPagesProject.Tests Utilisé pour tester l’intégration du ST.

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 répertoire tests/RazorPagesProject.Tests :

dotnet test

Organisation d’application de message (ST)

Le ST est un système de messages Pages Razor 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 (mots moyens 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).
  • Si la base de données est vide au démarrage de l’application, la banque de messages est initialisée avec trois messages.
  • L’application inclut un /SecurePage qui n’est accessible qu’à un utilisateur authentifié.

†L’article 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’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 (l’échantillon implémente le modèle de référentiel).

Tester l’organisation de l’application

L’application de test est une application console à l’intérieur du répertoire tests/RazorPagesProject.Tests.

Répertoire de l’application de test Description
AuthTests Contient des méthodes de test pour :
  • Accès à une page sécurisée par un utilisateur non authentifié.
  • Accès à une page sécurisée par un utilisateur authentifié avec un AuthenticationHandler<TOptions> fictif.
  • Obtention d’un profil utilisateur GitHub et vérification de la connexion utilisateur du profil.
BasicTests Contient une méthode de test pour le routage et le type de contenu.
IntegrationTests Contient les tests d’intégration pour la page Index à l’aide de la classe personnalisée WebApplicationFactory.
Helpers/Utilities
  • Utilities.cs contient la méthode InitializeDbForTests utilisée pour amorcer la base de données avec des données de test.
  • HtmlHelpers.cs fournit une méthode pour renvoyer un IHtmlDocument AngleSharp à utiliser avec les méthodes de test.
  • HttpClientExtensions.cs fournissent des surcharges pour SendAsync pour envoyer des demandes au ST.

Le framework de test est xUnit. Les tests d’intégration sont effectués à l’aide de Microsoft.AspNetCore.TestHost, qui inclut le TestServer. Étant donné que le package Microsoft.AspNetCore.Mvc.Testing est utilisé pour configurer l’hôte de test et le serveur de test, les packages TestHost et TestServer ne nécessitent pas de références de package directes dans le fichier projet de l’application de test ou la configuration du développeur dans l’application de test.

Les tests d’intégration nécessitent généralement un petit jeu de données dans la base de données avant l’exécution du test. Par exemple, un test de suppression appelle une suppression d’enregistrement de base de données, de sorte que la base de données doit avoir au moins un enregistrement pour que la requête de suppression aboutisse.

L’échantillon d’application amorcera la base de données avec trois messages dans Utilities.cs que les tests peuvent utiliser lorsqu’ils s’exécutent :

public static void InitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.AddRange(GetSeedingMessages());
    db.SaveChanges();
}

public static void ReinitializeDbForTests(ApplicationDbContext db)
{
    db.Messages.RemoveRange(db.Messages);
    InitializeDbForTests(db);
}

public static List<Message> GetSeedingMessages()
{
    return new List<Message>()
    {
        new Message(){ Text = "TEST RECORD: You're standing on my scarf." },
        new Message(){ Text = "TEST RECORD: Would you like a jelly baby?" },
        new Message(){ Text = "TEST RECORD: To the rational mind, " +
            "nothing is inexplicable; only unexplained." }
    };
}

Le contexte de base de données du ST est inscrit dans Program.cs. Le rappel de l’application de test builder.ConfigureServices est exécuté après l’exécution du code de l’application Program.cs. Pour utiliser une autre base de données pour les tests, le contexte de base de données de l’application doit être remplacé dans builder.ConfigureServices. Pour plus d’informations, consultez la section Personnaliser WebApplicationFactory .

Ressources supplémentaires