Condividi tramite


Test di integrazione in ASP.NET Core

Di Jos van der Til, Martin Costello e Patrick Calvarro Nelson.

I test di integrazione assicurano che i componenti di un'app funzionino correttamente a un livello che include l'infrastruttura di supporto dell'app, ad esempio il database, il file system e la rete. ASP.NET Core supporta i test di integrazione usando un framework di unit test con un host Web di test e un server di test in memoria.

Questo articolo presuppone una conoscenza di base degli unit test. Se non si ha familiarità con i concetti di test, vedere l'articolo Unit Testing in .NET Core e .NET Standard e il relativo contenuto collegato.

Visualizzare o scaricare il codice di esempio (procedura per il download)

L'app di esempio è un'app Razor Pages e presuppone una conoscenza di base delle Razor pagine. Se non si ha familiarità con Razor Pages, vedere gli articoli seguenti:

Per testare le applicazioni a pagina singola, è consigliabile usare uno strumento come Playwright per .NET, che può automatizzare un browser.

Introduzione ai test di integrazione

I test di integrazione valutano i componenti di un'app in un livello più ampio rispetto agli unit test. Gli unit test vengono usati per testare componenti software isolati, ad esempio singoli metodi di classe. I test di integrazione confermano che due o più componenti dell'app interagiscono per produrre un risultato previsto, possibilmente includendo ogni componente necessario per elaborare completamente una richiesta.

Questi test più ampi vengono usati per testare l'infrastruttura e l'intero framework dell'app, spesso inclusi i componenti seguenti:

  • Database
  • File system
  • Dispositivi di rete
  • Pipeline di richiesta-risposta

Gli unit test usano componenti creati, noti come falsi o oggetti fittizi, al posto dei componenti dell'infrastruttura.

A differenza degli unit test, i test di integrazione:

  • Usare i componenti effettivi usati dall'app nell'ambiente di produzione.
  • Richiedere più codice ed elaborazione dati.
  • L'esecuzione richiede più tempo.

Pertanto, limitare l'uso dei test di integrazione agli scenari di infrastruttura più importanti. Se un comportamento può essere testato usando uno unit test o un test di integrazione, scegliere lo unit test.

Nelle discussioni sui test di integrazione, il progetto testato viene spesso chiamato System Under Test o "SUT" per brevità. "SUT" viene usato in questo articolo per fare riferimento all'app ASP.NET Core sottoposta a test.

Non scrivere test di integrazione per ogni permutazione dei dati e dell'accesso ai file con database e file system. Indipendentemente dal numero di posizioni in cui un'app interagisce con database e file system, un set incentrato di test di integrazione di lettura, scrittura, aggiornamento ed eliminazione è in genere in grado di testare adeguatamente i componenti di database e file system. Usare unit test per i test di routine della logica del metodo che interagiscono con questi componenti. Negli unit test, l'uso di infrastrutture false o mocks comporta un'esecuzione più veloce dei test.

test di integrazione di ASP.NET Core

I test di integrazione in ASP.NET Core richiedono quanto segue:

  • Un progetto di test viene usato per contenere ed eseguire i test. Il progetto di test ha un riferimento a SUT.
  • Il progetto di test crea un host Web di test per SUT e usa un client del server di test per gestire le richieste e le risposte con SUT.
  • Un test runner viene usato per eseguire i test e segnalare i risultati del test.

I test di integrazione seguono una sequenza di eventi che includono i normali passaggi di test Arrange, Act e Assert :

  1. L'host Web SUT è configurato.
  2. Viene creato un client del server di test per inviare richieste all'app.
  3. Viene eseguito il passaggio Disponi test: l'app di test prepara una richiesta.
  4. Viene eseguito il passaggio di test act : il client invia la richiesta e riceve la risposta.
  5. Viene eseguito il passaggio di test Assert : la risposta effettiva viene convalidata come passaggio o esito negativo in base a una risposta prevista .
  6. Il processo continua fino a quando non vengono eseguiti tutti i test.
  7. I risultati del test vengono segnalati.

In genere, l'host Web di test viene configurato in modo diverso rispetto al normale host Web dell'app per le esecuzioni di test. Ad esempio, per i test è possibile usare un database diverso o impostazioni di app diverse.

I componenti dell'infrastruttura, ad esempio l'host Web di test e il server di test in memoria (TestServer), vengono forniti o gestiti dal pacchetto Microsoft.AspNetCore.Mvc.Testing. L'uso di questo pacchetto semplifica la creazione e l'esecuzione dei test.

Il Microsoft.AspNetCore.Mvc.Testing pacchetto gestisce le attività seguenti:

  • Copia il file delle dipendenze (.deps) dal SUT nella directory del progetto di bin test.
  • Imposta la radice del contenuto sulla radice del progetto di SUT in modo che i file statici e le pagine/visualizzazioni vengano trovati quando vengono eseguiti i test.
  • Fornisce la classe WebApplicationFactory per semplificare il bootstrap del SUT con TestServer.

La documentazione degli unit test descrive come configurare un progetto di test e uno strumento di esecuzione dei test, oltre a istruzioni dettagliate su come eseguire test e consigli su come assegnare un nome ai test e alle classi di test.

Separare gli unit test dai test di integrazione in progetti diversi. Separazione dei test:

  • Assicura che i componenti di test dell'infrastruttura non siano inclusi accidentalmente negli unit test.
  • Consente il controllo su quale set di test vengono eseguiti.

Non esiste praticamente alcuna differenza tra la configurazione per i test delle app Pages e delle Razor app MVC. L'unica differenza consiste nel modo in cui vengono denominati i test. In un'app Razor Pages, i test degli endpoint di pagina sono in genere denominati dopo la classe del modello di pagina, IndexPageTests ad esempio per testare l'integrazione dei componenti per la pagina Indice. In un'app MVC, i test sono in genere organizzati in base alle classi controller e denominati dopo i controller di cui eseguono il test, HomeControllerTests ad esempio per testare l'integrazione dei componenti per il Home controller.

Testare i prerequisiti dell'app

Il progetto di test deve:

Questi prerequisiti possono essere visualizzati nell'app di esempio. Esaminare il file tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. L'app di esempio usa il framework di test xUnit e la libreria parser AngleSharp , quindi l'app di esempio fa riferimento anche a:

Nelle app che usano xunit.runner.visualstudio la versione 2.4.2 o successiva, il progetto di test deve fare riferimento al Microsoft.NET.Test.Sdk pacchetto.

Entity Framework Core viene usato anche nei test. Vedere il file di progetto in GitHub.

Ambiente SUT

Se l'ambiente di SUT non è impostato, per impostazione predefinita l'ambiente è Sviluppo.

Test di base con WebApplicationFactory predefinito

Esporre la classe definita Program in modo implicito al progetto di test eseguendo una delle operazioni seguenti:

  • Esporre i tipi interni dall'app Web al progetto di test. Questa operazione può essere eseguita nel file del progetto SUT (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Rendere pubblica la Program classe usando una dichiarazione di classe parziale:

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

    L'app di esempio usa l'approccio Program di classe parziale.

WebApplicationFactory<TEntryPoint> viene usato per creare un TestServer oggetto per i test di integrazione. TEntryPoint è la classe del punto di ingresso di SUT, in genere Program.cs.

Le classi di test implementano un'interfaccia della fixture di classe (IClassFixture) per indicare che la classe contiene test e fornire istanze di oggetti condivisi tra i test nella classe .

La classe di test seguente, BasicTests, usa per WebApplicationFactory eseguire il bootstrap dell'oggetto SUT e fornire un HttpClient oggetto a un metodo di test, Get_EndpointsReturnSuccessAndCorrectContentType. Il metodo verifica che il codice di stato della risposta sia riuscito (200-299) e l'intestazione Content-Type sia text/html; charset=utf-8 per diverse pagine dell'app.

CreateClient() crea un'istanza di HttpClient che segue automaticamente i reindirizzamenti e gestisce i 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());
    }
}

Per impostazione predefinita, i cookie non essenziali non vengono mantenuti tra le richieste quando sono abilitati i criteri di consenso del Regolamento generale sulla protezione dei dati. Per conservare i cookie non essenziali, ad esempio quelli usati dal provider TempData, contrassegnarli come essenziali nei test. Per istruzioni su come contrassegnare un oggetto cookie come essenziale, vedere Cookie essenziali.

AngleSharp e Application Parts per i controlli antiforgery

Questo articolo usa il parser AngleSharp per gestire i controlli antiforgery caricando le pagine e analizzando il codice HTML. Per testare gli endpoint delle visualizzazioni controller e Razor Pages a un livello inferiore, senza preoccuparsi del modo in cui eseguono il rendering nel browser, è consigliabile usare Application Parts. L'approccio Parti dell'applicazione inserisce un controller o Razor una pagina nell'app che può essere usata per effettuare richieste JSON per ottenere i valori necessari. Per altre informazioni, vedere il blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts (Test di integrazione ASP.NET Risorse principali protette con Antiforgery Using Application Parts ) e il repository GitHub associato di Martin Costello.

Personalizzare WebApplicationFactory

La configurazione dell'host Web può essere creata indipendentemente dalle classi di test ereditando da WebApplicationFactory<TEntryPoint> per creare una o più factory personalizzate:

  1. Ereditare da WebApplicationFactory ed eseguire l'override ConfigureWebHostdi . IWebHostBuilder consente la configurazione della raccolta di servizi conIWebHostBuilder.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");
        }
    }
    

    Il seeding del database nell'app di esempio viene eseguito dal InitializeDbForTests metodo . Il metodo è descritto nella sezione Esempio di test di integrazione: Testare l'organizzazione dell'app.

    Il contesto del database di SUT viene registrato in Program.cs. Il callback dell'app di builder.ConfigureServices test viene eseguito dopo l'esecuzione del codice dell'app Program.cs . Per usare un database diverso per i test rispetto al database dell'app, il contesto del database dell'app deve essere sostituito in builder.ConfigureServices.

    L'app di esempio trova il descrittore del servizio per il contesto del database e usa il descrittore per rimuovere la registrazione del servizio. La factory aggiunge quindi un nuovo ApplicationDbContext oggetto che usa un database in memoria per i test.

    Per connettersi a un database diverso, modificare .DbConnection Per usare un database di test di SQL Server:

  1. Usare l'oggetto personalizzato CustomWebApplicationFactory nelle classi di test. Nell'esempio seguente viene usata la factory nella IndexPageTests classe :

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

    Il client dell'app di esempio è configurato per impedire i HttpClient reindirizzamenti seguenti. Come spiegato più avanti nella sezione Autenticazione fittizia , questo consente ai test di controllare il risultato della prima risposta dell'app. La prima risposta è un reindirizzamento in molti di questi test con un'intestazione Location .

  2. Un test tipico usa i HttpClient metodi helper e per elaborare la richiesta e la risposta:

    [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);
    }
    

Qualsiasi richiesta POST al SUT deve soddisfare il controllo antiforgery effettuato automaticamente dal sistema antiforgery di protezione dei dati dell'app. Per organizzare la richiesta POST di un test, l'app di test deve:

  1. Effettuare una richiesta per la pagina.
  2. Analizzare l'antiforgery cookie e richiedere il token di convalida dalla risposta.
  3. Effettuare la richiesta POST con il token di convalida antiforgery cookie e richiesta sul posto.

I SendAsync metodi di estensione helper (Helpers/HttpClientExtensions.cs) e il GetDocumentAsync metodo helper (Helpers/HtmlHelpers.cs) nell'app di esempio usano il parser AngleSharp per gestire il controllo antiforgery con i metodi seguenti:

  • GetDocumentAsync: riceve e restituisce HttpResponseMessage un oggetto IHtmlDocument. GetDocumentAsync usa una factory che prepara una risposta virtuale basata sull'originale HttpResponseMessage. Per altre informazioni, vedere la documentazione di AngleSharp.
  • SendAsync metodi di estensione per la HttpClient composizione di un HttpRequestMessage oggetto e una chiamata SendAsync(HttpRequestMessage) per inviare richieste al SUT. Overload per SendAsync accettare il modulo HTML (IHtmlFormElement) e gli elementi seguenti:
    • Pulsante Invia del modulo (IHtmlElement)
    • Insieme di valori modulo (IEnumerable<KeyValuePair<string, string>>)
    • Pulsante Invia (IHtmlElement) e valori modulo (IEnumerable<KeyValuePair<string, string>>)

AngleSharp è una libreria di analisi di terze parti usata a scopo dimostrativo in questo articolo e nell'app di esempio. AngleSharp non è supportato o necessario per il test di integrazione di app ASP.NET Core. È possibile usare altri parser, ad esempio Html Agility Pack (HAP). Un altro approccio consiste nel scrivere codice per gestire direttamente il token di verifica delle richieste del sistema antiforgery e antiforgery cookie . Per altre informazioni, vedere AngleSharp vs Application Parts for antiforgery check in questo articolo.

Il provider di database EF-Core in memoria può essere usato per test limitati e di base, ma il provider SQLite è la scelta consigliata per i test in memoria.

Vedere Estendere l'avvio con filtri di avvio che illustrano come configurare il middleware usando IStartupFilter, utile quando un test richiede un servizio personalizzato o un middleware.

Personalizzare il client con WithWebHostBuilder

Quando è necessaria una configurazione aggiuntiva all'interno di un metodo di test, WithWebHostBuilder crea un nuovo WebApplicationFactory oggetto con un IWebHostBuilder oggetto ulteriormente personalizzato in base alla configurazione.

Il codice di esempio chiama WithWebHostBuilder per sostituire i servizi configurati con stub di test. Per altre informazioni e l'utilizzo di esempio, vedere Inserire servizi fittizi in questo articolo.

Il Post_DeleteMessageHandler_ReturnsRedirectToRoot metodo di test dell'app di esempio illustra l'uso di WithWebHostBuilder. Questo test esegue un'eliminazione di record nel database attivando un invio di modulo nel SUT.

Poiché un altro test nella IndexPageTests classe esegue un'operazione che elimina tutti i record nel database e può essere eseguito prima del Post_DeleteMessageHandler_ReturnsRedirectToRoot metodo , il database viene reinviato in questo metodo di test per assicurarsi che un record sia presente per l'eliminazione di SUT. La selezione del primo pulsante di eliminazione del messages modulo nel SUT viene simulata nella richiesta al SUT:

[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);
}

Opzioni client

Per le impostazioni predefinite e le opzioni disponibili durante la creazione di HttpClient istanze, vedere la WebApplicationFactoryClientOptions pagina.

Creare la WebApplicationFactoryClientOptions classe e passarla al CreateClient() metodo :

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

NOTA: per evitare avvisi di reindirizzamento HTTPS nei log quando si usa il middleware di reindirizzamento HTTPS, impostare BaseAddress = new Uri("https://localhost")

Inserire servizi fittizi

I servizi possono essere sottoposti a override in un test con una chiamata a ConfigureTestServices nel generatore host. Per definire l'ambito dei servizi sottoposti a override al test stesso, il WithWebHostBuilder metodo viene usato per recuperare un generatore di host. Questo problema può essere visualizzato nei test seguenti:

L'esempio SUT include un servizio con ambito che restituisce un'offerta. L'offerta è incorporata in un campo nascosto nella pagina Indice quando viene richiesta la pagina Indice.

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">

Quando viene eseguita l'app SUT, viene generato il markup seguente:

<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.">

Per testare il servizio e l'inserimento di virgolette in un test di integrazione, un servizio fittizio viene inserito nel SUT dal test. Il servizio fittizio sostituisce l'app QuoteService con un servizio fornito dall'app di test, denominato 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 viene chiamato e il servizio con ambito è registrato:

[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);
}

Il markup generato durante l'esecuzione del test riflette il testo delle virgolette fornito da TestQuoteService, quindi l'asserzione supera:

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

Autenticazione fittizia

I test nella AuthTests classe verificano che un endpoint sicuro:

  • Reindirizza un utente non autenticato alla pagina di accesso dell'app.
  • Restituisce il contenuto per un utente autenticato.

In SUT la /SecurePage pagina usa una AuthorizePage convenzione per applicare un oggetto AuthorizeFilter alla pagina. Per altre informazioni, vedere Razor Convenzioni di autorizzazione pagine.

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

Get_SecurePageRedirectsAnUnauthenticatedUser Nel test un WebApplicationFactoryClientOptions oggetto è impostato su non consentire i reindirizzamenti impostando AllowAutoRedirect su 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);
}

Non consentendo al client di seguire il reindirizzamento, è possibile effettuare i controlli seguenti:

  • Il codice di stato restituito da SUT può essere controllato rispetto al risultato previsto HttpStatusCode.Redirect , non al codice di stato finale dopo il reindirizzamento alla pagina di accesso, che sarebbe HttpStatusCode.OK.
  • Il Location valore dell'intestazione nelle intestazioni della risposta viene controllato per confermare che inizia con http://localhost/Identity/Account/Login, non la risposta finale della pagina di accesso, in cui l'intestazione Location non sarebbe presente.

L'app di test può simulare un oggetto AuthenticationHandler<TOptions> ConfigureTestServices per testare gli aspetti dell'autenticazione e dell'autorizzazione. Uno scenario minimo restituisce :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);
    }
}

Viene TestAuthHandler chiamato per autenticare un utente quando lo schema di autenticazione è impostato su TestScheme dove AddAuthentication è registrato per ConfigureTestServices. È importante che lo TestScheme schema corrisponda allo schema previsto dall'app. In caso contrario, l'autenticazione non funzionerà.

[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);
}

Per altre informazioni su WebApplicationFactoryClientOptions, vedere la sezione Opzioni client.

Test di base per il middleware di autenticazione

Vedere questo repository GitHub per i test di base del middleware di autenticazione. Contiene un server di test specifico dello scenario di test.

Impostare l'ambiente

Impostare l'ambiente nella factory dell'applicazione personalizzata:

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");
    }
}

Come l'infrastruttura di test deduce il percorso radice del contenuto dell'app

Il WebApplicationFactory costruttore deduce il percorso radice del contenuto dell'app cercando un WebApplicationFactoryContentRootAttribute nell'assembly contenente i test di integrazione con una chiave uguale all'assembly TEntryPoint System.Reflection.Assembly.FullName. Se non viene trovato un attributo con la chiave corretta, WebApplicationFactory esegue nuovamente la ricerca di un file di soluzione (.sln) e aggiunge il nome dell'assembly TEntryPoint alla directory della soluzione. La directory radice dell'app (percorso radice del contenuto) viene usata per individuare visualizzazioni e file di contenuto.

Disabilitare la copia shadow

La copia shadow fa sì che i test vengano eseguiti in una directory diversa rispetto alla directory di output. Se i test si basano sul caricamento di file relativi a Assembly.Location e si verificano problemi, potrebbe essere necessario disabilitare la copia shadow.

Per disabilitare la copia shadow quando si usa xUnit, creare un xunit.runner.json file nella directory del progetto di test con l'impostazione di configurazione corretta:

{
  "shadowCopy": false
}

Eliminazione di oggetti

Dopo l'esecuzione TestServer dei test dell'implementazione IClassFixture e HttpClient vengono eliminati quando xUnit elimina .WebApplicationFactory Se gli oggetti creati dallo sviluppatore richiedono l'eliminazione, eliminarli nell'implementazione IClassFixture . Per altre informazioni, vedere Implementazione di un metodo Dispose.

Esempio di test di integrazione

L'app di esempio è costituita da due app:

App Directory del progetto Descrizione
App messaggio (SUT) src/RazorPagesProject Consente a un utente di aggiungere, eliminare, eliminare tutti e analizzare i messaggi.
Testare l'app tests/RazorPagesProject.Tests Usato per eseguire il test dell'integrazione di SUT.

I test possono essere eseguiti usando le funzionalità di test predefinite di un IDE, ad esempio Visual Studio. Se si usa Visual Studio Code o la riga di comando, eseguire il comando seguente al prompt dei comandi nella tests/RazorPagesProject.Tests directory:

dotnet test

Organizzazione dell'app message (SUT)

SUT è un Razor sistema di messaggi Pages con le caratteristiche seguenti:

  • La pagina Index dell'app (Pages/Index.cshtml e Pages/Index.cshtml.cs) fornisce metodi di interfaccia utente e modello di pagina per controllare l'aggiunta, l'eliminazione e l'analisi dei messaggi (parole medie per messaggio).
  • Un messaggio viene descritto dalla Message classe (Data/Message.cs) con due proprietà: Id (chiave) e Text (messaggio). La Text proprietà è obbligatoria e limitata a 200 caratteri.
  • I messaggi vengono archiviati usando il database in memoria di Entity Framework†.
  • L'app contiene un livello di accesso ai dati (DAL) nella classe di contesto del database (AppDbContextData/AppDbContext.cs).
  • Se il database è vuoto all'avvio dell'app, l'archivio messaggi viene inizializzato con tre messaggi.
  • L'app include un oggetto /SecurePage accessibile solo da un utente autenticato.

†Il test di Entity Framework con InMemory illustra come usare un database in memoria per i test con MSTest. Questo argomento usa il framework di test xUnit . I concetti di test e le implementazioni di test in framework di test diversi sono simili ma non identici.

Anche se l'app non usa il modello di repository e non è un esempio efficace del modello Unit of Work (UoW), Razor Pages supporta questi modelli di sviluppo. Per altre informazioni, vedere Progettazione del livello di persistenza dell'infrastruttura e Logica del controller di test (l'esempio implementa il modello di repository).

Testare l'organizzazione dell'app

L'app di test è un'app console all'interno della tests/RazorPagesProject.Tests directory.

Testare la directory dell'app Descrizione
AuthTests Contiene i metodi di test per:
  • Accesso a una pagina protetta da un utente non autenticato.
  • Accesso a una pagina sicura da parte di un utente autenticato con un fittizio AuthenticationHandler<TOptions>.
  • Ottenere un profilo utente GitHub e controllare l'account di accesso utente del profilo.
BasicTests Contiene un metodo di test per il routing e il tipo di contenuto.
IntegrationTests Contiene i test di integrazione per la pagina Index usando la classe personalizzata WebApplicationFactory .
Helpers/Utilities
  • Utilities.cs contiene il metodo utilizzato per eseguire il InitializeDbForTests seeding del database con i dati di test.
  • HtmlHelpers.cs fornisce un metodo per restituire un AngleSharp IHtmlDocument da utilizzare dai metodi di test.
  • HttpClientExtensions.cs fornire overload per SendAsync inviare richieste al SUT.

Il framework di test è xUnit. I test di integrazione vengono eseguiti usando , Microsoft.AspNetCore.TestHostche include .TestServer Poiché il Microsoft.AspNetCore.Mvc.Testing pacchetto viene usato per configurare l'host di test e il server di test, i TestHost pacchetti e TestServer non richiedono riferimenti diretti al pacchetto nel file di progetto o nella configurazione dello sviluppatore dell'app di test nell'app di test.

I test di integrazione richiedono in genere un set di dati di piccole dimensioni nel database prima dell'esecuzione del test. Ad esempio, una chiamata di test di eliminazione per l'eliminazione di un record di database, pertanto il database deve avere almeno un record affinché la richiesta di eliminazione abbia esito positivo.

L'app di esempio esegue il seeding del database con tre messaggi in Utilities.cs che i test possono usare quando vengono eseguiti:

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." }
    };
}

Il contesto del database di SUT viene registrato in Program.cs. Il callback dell'app di builder.ConfigureServices test viene eseguito dopo l'esecuzione del codice dell'app Program.cs . Per usare un database diverso per i test, il contesto del database dell'app deve essere sostituito in builder.ConfigureServices. Per altre informazioni, vedere la sezione Customize WebApplicationFactory .

Risorse aggiuntive

In questo argomento si presuppone una conoscenza di base degli unit test. Se non si ha familiarità con i concetti di test, vedere l'argomento Unit Testing in .NET Core e .NET Standard e il relativo contenuto collegato.

Visualizzare o scaricare il codice di esempio (procedura per il download)

L'app di esempio è un'app Razor Pages e presuppone una conoscenza di base delle Razor pagine. Se non si ha familiarità con Razor Pages, vedere gli argomenti seguenti:

Nota

Per testare le applicazioni a pagina singola, è consigliabile usare uno strumento come Playwright per .NET, che può automatizzare un browser.

Introduzione ai test di integrazione

I test di integrazione valutano i componenti di un'app in un livello più ampio rispetto agli unit test. Gli unit test vengono usati per testare componenti software isolati, ad esempio singoli metodi di classe. I test di integrazione confermano che due o più componenti dell'app interagiscono per produrre un risultato previsto, possibilmente includendo ogni componente necessario per elaborare completamente una richiesta.

Questi test più ampi vengono usati per testare l'infrastruttura e l'intero framework dell'app, spesso inclusi i componenti seguenti:

  • Database
  • File system
  • Dispositivi di rete
  • Pipeline di richiesta-risposta

Gli unit test usano componenti creati, noti come falsi o oggetti fittizi, al posto dei componenti dell'infrastruttura.

A differenza degli unit test, i test di integrazione:

  • Usare i componenti effettivi usati dall'app nell'ambiente di produzione.
  • Richiedere più codice ed elaborazione dati.
  • L'esecuzione richiede più tempo.

Pertanto, limitare l'uso dei test di integrazione agli scenari di infrastruttura più importanti. Se un comportamento può essere testato usando uno unit test o un test di integrazione, scegliere lo unit test.

Nelle discussioni sui test di integrazione, il progetto testato viene spesso chiamato System Under Test o "SUT" per brevità. "SUT" viene usato in questo articolo per fare riferimento all'app ASP.NET Core sottoposta a test.

Non scrivere test di integrazione per ogni permutazione dei dati e dell'accesso ai file con database e file system. Indipendentemente dal numero di posizioni in cui un'app interagisce con database e file system, un set incentrato di test di integrazione di lettura, scrittura, aggiornamento ed eliminazione è in genere in grado di testare adeguatamente i componenti di database e file system. Usare unit test per i test di routine della logica del metodo che interagiscono con questi componenti. Negli unit test, l'uso di infrastrutture false o mocks comporta un'esecuzione più veloce dei test.

test di integrazione di ASP.NET Core

I test di integrazione in ASP.NET Core richiedono quanto segue:

  • Un progetto di test viene usato per contenere ed eseguire i test. Il progetto di test ha un riferimento a SUT.
  • Il progetto di test crea un host Web di test per SUT e usa un client del server di test per gestire le richieste e le risposte con SUT.
  • Un test runner viene usato per eseguire i test e segnalare i risultati del test.

I test di integrazione seguono una sequenza di eventi che includono i normali passaggi di test Arrange, Act e Assert :

  1. L'host Web SUT è configurato.
  2. Viene creato un client del server di test per inviare richieste all'app.
  3. Viene eseguito il passaggio Disponi test: l'app di test prepara una richiesta.
  4. Viene eseguito il passaggio di test act : il client invia la richiesta e riceve la risposta.
  5. Viene eseguito il passaggio di test Assert : la risposta effettiva viene convalidata come passaggio o esito negativo in base a una risposta prevista .
  6. Il processo continua fino a quando non vengono eseguiti tutti i test.
  7. I risultati del test vengono segnalati.

In genere, l'host Web di test viene configurato in modo diverso rispetto al normale host Web dell'app per le esecuzioni di test. Ad esempio, per i test è possibile usare un database diverso o impostazioni di app diverse.

I componenti dell'infrastruttura, ad esempio l'host Web di test e il server di test in memoria (TestServer), vengono forniti o gestiti dal pacchetto Microsoft.AspNetCore.Mvc.Testing. L'uso di questo pacchetto semplifica la creazione e l'esecuzione dei test.

Il Microsoft.AspNetCore.Mvc.Testing pacchetto gestisce le attività seguenti:

  • Copia il file delle dipendenze (.deps) dal SUT nella directory del progetto di bin test.
  • Imposta la radice del contenuto sulla radice del progetto di SUT in modo che i file statici e le pagine/visualizzazioni vengano trovati quando vengono eseguiti i test.
  • Fornisce la classe WebApplicationFactory per semplificare il bootstrap del SUT con TestServer.

La documentazione degli unit test descrive come configurare un progetto di test e uno strumento di esecuzione dei test, oltre a istruzioni dettagliate su come eseguire test e consigli su come assegnare un nome ai test e alle classi di test.

Separare gli unit test dai test di integrazione in progetti diversi. Separazione dei test:

  • Assicura che i componenti di test dell'infrastruttura non siano inclusi accidentalmente negli unit test.
  • Consente il controllo su quale set di test vengono eseguiti.

Non esiste praticamente alcuna differenza tra la configurazione per i test delle app Pages e delle Razor app MVC. L'unica differenza consiste nel modo in cui vengono denominati i test. In un'app Razor Pages, i test degli endpoint di pagina sono in genere denominati dopo la classe del modello di pagina, IndexPageTests ad esempio per testare l'integrazione dei componenti per la pagina Indice. In un'app MVC, i test sono in genere organizzati in base alle classi controller e denominati dopo i controller di cui eseguono il test, HomeControllerTests ad esempio per testare l'integrazione dei componenti per il Home controller.

Testare i prerequisiti dell'app

Il progetto di test deve:

Questi prerequisiti possono essere visualizzati nell'app di esempio. Esaminare il file tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. L'app di esempio usa il framework di test xUnit e la libreria parser AngleSharp , quindi l'app di esempio fa riferimento anche a:

Nelle app che usano xunit.runner.visualstudio la versione 2.4.2 o successiva, il progetto di test deve fare riferimento al Microsoft.NET.Test.Sdk pacchetto.

Entity Framework Core viene usato anche nei test. Riferimenti all'app:

Ambiente SUT

Se l'ambiente di SUT non è impostato, per impostazione predefinita l'ambiente è Sviluppo.

Test di base con WebApplicationFactory predefinito

WebApplicationFactory<TEntryPoint> viene usato per creare un TestServer oggetto per i test di integrazione. TEntryPoint è la classe del punto di ingresso del SUT, in genere la Startup classe .

Le classi di test implementano un'interfaccia della fixture di classe (IClassFixture) per indicare che la classe contiene test e fornire istanze di oggetti condivisi tra i test nella classe .

La classe di test seguente, BasicTests, usa per WebApplicationFactory eseguire il bootstrap dell'oggetto SUT e fornire un HttpClient oggetto a un metodo di test, Get_EndpointsReturnSuccessAndCorrectContentType. Il metodo verifica se il codice di stato della risposta ha esito positivo (codici di stato nell'intervallo 200-299) e l'intestazione Content-Type è text/html; charset=utf-8 per diverse pagine dell'app.

CreateClient() crea un'istanza di HttpClient che segue automaticamente i reindirizzamenti e gestisce i 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());
    }
}

Per impostazione predefinita, i cookie non essenziali non vengono mantenuti tra le richieste quando i criteri di consenso gdpr sono abilitati. Per conservare i cookie non essenziali, ad esempio quelli usati dal provider TempData, contrassegnarli come essenziali nei test. Per istruzioni su come contrassegnare un oggetto cookie come essenziale, vedere Cookie essenziali.

Personalizzare WebApplicationFactory

La configurazione dell'host Web può essere creata indipendentemente dalle classi di test ereditando da WebApplicationFactory per creare una o più factory personalizzate:

  1. Ereditare da WebApplicationFactory ed eseguire l'override ConfigureWebHostdi . IWebHostBuilder consente la configurazione della raccolta di servizi con 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);
                    }
                }
            });
        }
    }
    

    Il seeding del database nell'app di esempio viene eseguito dal InitializeDbForTests metodo . Il metodo è descritto nella sezione Esempio di test di integrazione: Testare l'organizzazione dell'app.

    Il contesto del database di SUT viene registrato nel relativo Startup.ConfigureServices metodo. Il callback dell'app di builder.ConfigureServices test viene eseguito dopo l'esecuzione del codice dell'app Startup.ConfigureServices . L'ordine di esecuzione è una modifica di rilievo per l'host generico con il rilascio di ASP.NET Core 3.0. Per usare un database diverso per i test rispetto al database dell'app, il contesto del database dell'app deve essere sostituito in builder.ConfigureServices.

    Per i sut che usano ancora l'host Web, il callback dell'app di builder.ConfigureServices test viene eseguito prima del codice di Startup.ConfigureServices SUT. Il callback dell'app di builder.ConfigureTestServices test viene eseguito dopo.

    L'app di esempio trova il descrittore del servizio per il contesto del database e usa il descrittore per rimuovere la registrazione del servizio. Successivamente, la factory aggiunge un nuovo ApplicationDbContext oggetto che usa un database in memoria per i test.

    Per connettersi a un database diverso rispetto al database in memoria, modificare la UseInMemoryDatabase chiamata per connettere il contesto a un database diverso. Per usare un database di test di SQL Server:

    services.AddDbContext<ApplicationDbContext>((options, context) => 
    {
        context.UseSqlServer(
            Configuration.GetConnectionString("TestingDbConnectionString"));
    });
    
  2. Usare l'oggetto personalizzato CustomWebApplicationFactory nelle classi di test. Nell'esempio seguente viene usata la factory nella IndexPageTests classe :

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

    Il client dell'app di esempio è configurato per impedire i HttpClient reindirizzamenti seguenti. Come spiegato più avanti nella sezione Autenticazione fittizia , questo consente ai test di controllare il risultato della prima risposta dell'app. La prima risposta è un reindirizzamento in molti di questi test con un'intestazione Location .

  3. Un test tipico usa i HttpClient metodi helper e per elaborare la richiesta e la risposta:

    [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);
    }
    

Qualsiasi richiesta POST al SUT deve soddisfare il controllo antiforgery effettuato automaticamente dal sistema antiforgery di protezione dei dati dell'app. Per organizzare la richiesta POST di un test, l'app di test deve:

  1. Effettuare una richiesta per la pagina.
  2. Analizzare l'antiforgery cookie e richiedere il token di convalida dalla risposta.
  3. Effettuare la richiesta POST con il token di convalida antiforgery cookie e richiesta sul posto.

I SendAsync metodi di estensione helper (Helpers/HttpClientExtensions.cs) e il GetDocumentAsync metodo helper (Helpers/HtmlHelpers.cs) nell'app di esempio usano il parser AngleSharp per gestire il controllo antiforgery con i metodi seguenti:

  • GetDocumentAsync: riceve e restituisce HttpResponseMessage un oggetto IHtmlDocument. GetDocumentAsync usa una factory che prepara una risposta virtuale basata sull'originale HttpResponseMessage. Per altre informazioni, vedere la documentazione di AngleSharp.
  • SendAsync metodi di estensione per la HttpClient composizione di un HttpRequestMessage oggetto e una chiamata SendAsync(HttpRequestMessage) per inviare richieste al SUT. Overload per SendAsync accettare il modulo HTML (IHtmlFormElement) e gli elementi seguenti:
    • Pulsante Invia del modulo (IHtmlElement)
    • Insieme di valori modulo (IEnumerable<KeyValuePair<string, string>>)
    • Pulsante Invia (IHtmlElement) e valori modulo (IEnumerable<KeyValuePair<string, string>>)

Nota

AngleSharp è una libreria di analisi di terze parti usata a scopo dimostrativo in questo argomento e nell'app di esempio. AngleSharp non è supportato o necessario per il test di integrazione di app ASP.NET Core. È possibile usare altri parser, ad esempio Html Agility Pack (HAP). Un altro approccio consiste nel scrivere codice per gestire direttamente il token di verifica delle richieste del sistema antiforgery e antiforgery cookie .

Nota

Il provider di database EF-Core in memoria può essere usato per test limitati e di base, ma il provider SQLite è la scelta consigliata per i test in memoria.

Personalizzare il client con WithWebHostBuilder

Quando è necessaria una configurazione aggiuntiva all'interno di un metodo di test, WithWebHostBuilder crea un nuovo WebApplicationFactory oggetto con un IWebHostBuilder oggetto ulteriormente personalizzato in base alla configurazione.

Il Post_DeleteMessageHandler_ReturnsRedirectToRoot metodo di test dell'app di esempio illustra l'uso di WithWebHostBuilder. Questo test esegue un'eliminazione di record nel database attivando un invio di modulo nel SUT.

Poiché un altro test nella IndexPageTests classe esegue un'operazione che elimina tutti i record nel database e può essere eseguito prima del Post_DeleteMessageHandler_ReturnsRedirectToRoot metodo , il database viene reinviato in questo metodo di test per assicurarsi che un record sia presente per l'eliminazione di SUT. La selezione del primo pulsante di eliminazione del messages modulo nel SUT viene simulata nella richiesta al SUT:

[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);
}

Opzioni client

Nella tabella seguente viene illustrata l'impostazione predefinita WebApplicationFactoryClientOptions disponibile durante la creazione di HttpClient istanze.

Opzione Descrizione Default
AllowAutoRedirect Ottiene o imposta un valore che indica se HttpClient le istanze devono seguire automaticamente le risposte di reindirizzamento. true
BaseAddress Ottiene o imposta l'indirizzo di base delle HttpClient istanze. http://localhost
HandleCookies Ottiene o imposta un valore che indica se HttpClient le istanze devono gestire i cookie. true
MaxAutomaticRedirections Ottiene o imposta il numero massimo di risposte di reindirizzamento che HttpClient devono essere seguite dalle istanze. 7

Creare la WebApplicationFactoryClientOptions classe e passarla al CreateClient() metodo (i valori predefiniti sono visualizzati nell'esempio di codice):

// 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);

Inserire servizi fittizi

I servizi possono essere sottoposti a override in un test con una chiamata a ConfigureTestServices nel generatore host. Per inserire servizi fittizi, il SUT deve avere una Startup classe con un Startup.ConfigureServices metodo .

L'esempio SUT include un servizio con ambito che restituisce un'offerta. L'offerta è incorporata in un campo nascosto nella pagina Indice quando viene richiesta la pagina Indice.

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">

Quando viene eseguita l'app SUT, viene generato il markup seguente:

<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.">

Per testare il servizio e l'inserimento di virgolette in un test di integrazione, un servizio fittizio viene inserito nel SUT dal test. Il servizio fittizio sostituisce l'app QuoteService con un servizio fornito dall'app di test, denominato 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 viene chiamato e il servizio con ambito è registrato:

[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);
}

Il markup generato durante l'esecuzione del test riflette il testo delle virgolette fornito da TestQuoteService, quindi l'asserzione supera:

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

Autenticazione fittizia

I test nella AuthTests classe verificano che un endpoint sicuro:

  • Reindirizza un utente non autenticato alla pagina di accesso dell'app.
  • Restituisce il contenuto per un utente autenticato.

In SUT la /SecurePage pagina usa una AuthorizePage convenzione per applicare un oggetto AuthorizeFilter alla pagina. Per altre informazioni, vedere Razor Convenzioni di autorizzazione pagine.

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

Get_SecurePageRedirectsAnUnauthenticatedUser Nel test un WebApplicationFactoryClientOptions oggetto è impostato su non consentire i reindirizzamenti impostando AllowAutoRedirect su 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);
}

Non consentendo al client di seguire il reindirizzamento, è possibile effettuare i controlli seguenti:

  • Il codice di stato restituito da SUT può essere controllato rispetto al risultato previsto HttpStatusCode.Redirect , non al codice di stato finale dopo il reindirizzamento alla pagina Di accesso, che sarebbe HttpStatusCode.OK.
  • Il Location valore dell'intestazione nelle intestazioni di risposta viene controllato per confermare che inizia con http://localhost/Identity/Account/Login, non la risposta finale della pagina di accesso, in cui l'intestazione Location non sarebbe presente.

L'app di test può simulare un oggetto AuthenticationHandler<TOptions> ConfigureTestServices per testare gli aspetti dell'autenticazione e dell'autorizzazione. Uno scenario minimo restituisce :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);
    }
}

Viene TestAuthHandler chiamato per autenticare un utente quando lo schema di autenticazione è impostato su Test dove AddAuthentication è registrato per ConfigureTestServices. È importante che lo Test schema corrisponda allo schema previsto dall'app. In caso contrario, l'autenticazione non funzionerà.

[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);
}

Per altre informazioni su WebApplicationFactoryClientOptions, vedere la sezione Opzioni client.

Impostare l'ambiente

Per impostazione predefinita, l'host e l'ambiente app di SUT sono configurati per l'uso dell'ambiente di sviluppo. Per eseguire l'override dell'ambiente di SUT quando si usa IHostBuilder:

  • Impostare la ASPNETCORE_ENVIRONMENT variabile di ambiente , ad esempio , StagingProductiono un altro valore personalizzato, ad esempio Testing.
  • Eseguire l'override nell'app di test per leggere le variabili di ambiente precedute CreateHostBuilder da ASPNETCORE.
protected override IHostBuilder CreateHostBuilder() =>
    base.CreateHostBuilder()
        .ConfigureHostConfiguration(
            config => config.AddEnvironmentVariables("ASPNETCORE"));

Se suT usa l'host Web (IWebHostBuilder), eseguire l'override CreateWebHostBuilderdi :

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

Come l'infrastruttura di test deduce il percorso radice del contenuto dell'app

Il WebApplicationFactory costruttore deduce il percorso radice del contenuto dell'app cercando un WebApplicationFactoryContentRootAttribute nell'assembly contenente i test di integrazione con una chiave uguale all'assembly TEntryPoint System.Reflection.Assembly.FullName. Se non viene trovato un attributo con la chiave corretta, WebApplicationFactory esegue nuovamente la ricerca di un file di soluzione (.sln) e aggiunge il nome dell'assembly TEntryPoint alla directory della soluzione. La directory radice dell'app (percorso radice del contenuto) viene usata per individuare visualizzazioni e file di contenuto.

Disabilitare la copia shadow

La copia shadow fa sì che i test vengano eseguiti in una directory diversa rispetto alla directory di output. Se i test si basano sul caricamento di file relativi a Assembly.Location e si verificano problemi, potrebbe essere necessario disabilitare la copia shadow.

Per disabilitare la copia shadow quando si usa xUnit, creare un xunit.runner.json file nella directory del progetto di test con l'impostazione di configurazione corretta:

{
  "shadowCopy": false
}

Eliminazione di oggetti

Dopo l'esecuzione TestServer dei test dell'implementazione IClassFixture e HttpClient vengono eliminati quando xUnit elimina .WebApplicationFactory Se gli oggetti creati dallo sviluppatore richiedono l'eliminazione, eliminarli nell'implementazione IClassFixture . Per altre informazioni, vedere Implementazione di un metodo Dispose.

Esempio di test di integrazione

L'app di esempio è costituita da due app:

App Directory del progetto Descrizione
App messaggio (SUT) src/RazorPagesProject Consente a un utente di aggiungere, eliminare, eliminare tutti e analizzare i messaggi.
Testare l'app tests/RazorPagesProject.Tests Usato per eseguire il test dell'integrazione di SUT.

I test possono essere eseguiti usando le funzionalità di test predefinite di un IDE, ad esempio Visual Studio. Se si usa Visual Studio Code o la riga di comando, eseguire il comando seguente al prompt dei comandi nella tests/RazorPagesProject.Tests directory:

dotnet test

Organizzazione dell'app message (SUT)

SUT è un Razor sistema di messaggi Pages con le caratteristiche seguenti:

  • La pagina Index dell'app (Pages/Index.cshtml e Pages/Index.cshtml.cs) fornisce metodi di interfaccia utente e modello di pagina per controllare l'aggiunta, l'eliminazione e l'analisi dei messaggi (parole medie per messaggio).
  • Un messaggio viene descritto dalla Message classe (Data/Message.cs) con due proprietà: Id (chiave) e Text (messaggio). La Text proprietà è obbligatoria e limitata a 200 caratteri.
  • I messaggi vengono archiviati usando il database in memoria di Entity Framework†.
  • L'app contiene un livello di accesso ai dati (DAL) nella classe di contesto del database (AppDbContextData/AppDbContext.cs).
  • Se il database è vuoto all'avvio dell'app, l'archivio messaggi viene inizializzato con tre messaggi.
  • L'app include un oggetto /SecurePage accessibile solo da un utente autenticato.

†Il test di Entity Framework con InMemory illustra come usare un database in memoria per i test con MSTest. Questo argomento usa il framework di test xUnit . I concetti di test e le implementazioni di test in framework di test diversi sono simili ma non identici.

Anche se l'app non usa il modello di repository e non è un esempio efficace del modello Unit of Work (UoW), Razor Pages supporta questi modelli di sviluppo. Per altre informazioni, vedere Progettazione del livello di persistenza dell'infrastruttura e Logica del controller di test (l'esempio implementa il modello di repository).

Testare l'organizzazione dell'app

L'app di test è un'app console all'interno della tests/RazorPagesProject.Tests directory.

Testare la directory dell'app Descrizione
AuthTests Contiene i metodi di test per:
  • Accesso a una pagina protetta da un utente non autenticato.
  • Accesso a una pagina sicura da parte di un utente autenticato con un fittizio AuthenticationHandler<TOptions>.
  • Ottenere un profilo utente GitHub e controllare l'account di accesso utente del profilo.
BasicTests Contiene un metodo di test per il routing e il tipo di contenuto.
IntegrationTests Contiene i test di integrazione per la pagina Index usando la classe personalizzata WebApplicationFactory .
Helpers/Utilities
  • Utilities.cs contiene il metodo utilizzato per eseguire il InitializeDbForTests seeding del database con i dati di test.
  • HtmlHelpers.cs fornisce un metodo per restituire un AngleSharp IHtmlDocument da utilizzare dai metodi di test.
  • HttpClientExtensions.cs fornire overload per SendAsync inviare richieste al SUT.

Il framework di test è xUnit. I test di integrazione vengono eseguiti usando , Microsoft.AspNetCore.TestHostche include .TestServer Poiché il Microsoft.AspNetCore.Mvc.Testing pacchetto viene usato per configurare l'host di test e il server di test, i TestHost pacchetti e TestServer non richiedono riferimenti diretti al pacchetto nel file di progetto o nella configurazione dello sviluppatore dell'app di test nell'app di test.

I test di integrazione richiedono in genere un set di dati di piccole dimensioni nel database prima dell'esecuzione del test. Ad esempio, una chiamata di test di eliminazione per l'eliminazione di un record di database, pertanto il database deve avere almeno un record affinché la richiesta di eliminazione abbia esito positivo.

L'app di esempio esegue il seeding del database con tre messaggi in Utilities.cs che i test possono usare quando vengono eseguiti:

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." }
    };
}

Il contesto del database di SUT viene registrato nel relativo Startup.ConfigureServices metodo. Il callback dell'app di builder.ConfigureServices test viene eseguito dopo l'esecuzione del codice dell'app Startup.ConfigureServices . Per usare un database diverso per i test, il contesto del database dell'app deve essere sostituito in builder.ConfigureServices. Per altre informazioni, vedere la sezione Customize WebApplicationFactory .

Per i sut che usano ancora l'host Web, il callback dell'app di builder.ConfigureServices test viene eseguito prima del codice di Startup.ConfigureServices SUT. Il callback dell'app di builder.ConfigureTestServices test viene eseguito dopo.

Risorse aggiuntive

Questo articolo presuppone una conoscenza di base degli unit test. Se non si ha familiarità con i concetti di test, vedere l'articolo Unit Testing in .NET Core e .NET Standard e il relativo contenuto collegato.

Visualizzare o scaricare il codice di esempio (procedura per il download)

L'app di esempio è un'app Razor Pages e presuppone una conoscenza di base delle Razor pagine. Se non si ha familiarità con Razor Pages, vedere gli articoli seguenti:

Per testare le applicazioni a pagina singola, è consigliabile usare uno strumento come Playwright per .NET, che può automatizzare un browser.

Introduzione ai test di integrazione

I test di integrazione valutano i componenti di un'app in un livello più ampio rispetto agli unit test. Gli unit test vengono usati per testare componenti software isolati, ad esempio singoli metodi di classe. I test di integrazione confermano che due o più componenti dell'app interagiscono per produrre un risultato previsto, possibilmente includendo ogni componente necessario per elaborare completamente una richiesta.

Questi test più ampi vengono usati per testare l'infrastruttura e l'intero framework dell'app, spesso inclusi i componenti seguenti:

  • Database
  • File system
  • Dispositivi di rete
  • Pipeline di richiesta-risposta

Gli unit test usano componenti creati, noti come falsi o oggetti fittizi, al posto dei componenti dell'infrastruttura.

A differenza degli unit test, i test di integrazione:

  • Usare i componenti effettivi usati dall'app nell'ambiente di produzione.
  • Richiedere più codice ed elaborazione dati.
  • L'esecuzione richiede più tempo.

Pertanto, limitare l'uso dei test di integrazione agli scenari di infrastruttura più importanti. Se un comportamento può essere testato usando uno unit test o un test di integrazione, scegliere lo unit test.

Nelle discussioni sui test di integrazione, il progetto testato viene spesso chiamato System Under Test o "SUT" per brevità. "SUT" viene usato in questo articolo per fare riferimento all'app ASP.NET Core sottoposta a test.

Non scrivere test di integrazione per ogni permutazione dei dati e dell'accesso ai file con database e file system. Indipendentemente dal numero di posizioni in cui un'app interagisce con database e file system, un set incentrato di test di integrazione di lettura, scrittura, aggiornamento ed eliminazione è in genere in grado di testare adeguatamente i componenti di database e file system. Usare unit test per i test di routine della logica del metodo che interagiscono con questi componenti. Negli unit test, l'uso di infrastrutture false o mocks comporta un'esecuzione più veloce dei test.

test di integrazione di ASP.NET Core

I test di integrazione in ASP.NET Core richiedono quanto segue:

  • Un progetto di test viene usato per contenere ed eseguire i test. Il progetto di test ha un riferimento a SUT.
  • Il progetto di test crea un host Web di test per SUT e usa un client del server di test per gestire le richieste e le risposte con SUT.
  • Un test runner viene usato per eseguire i test e segnalare i risultati del test.

I test di integrazione seguono una sequenza di eventi che includono i normali passaggi di test Arrange, Act e Assert :

  1. L'host Web SUT è configurato.
  2. Viene creato un client del server di test per inviare richieste all'app.
  3. Viene eseguito il passaggio Disponi test: l'app di test prepara una richiesta.
  4. Viene eseguito il passaggio di test act : il client invia la richiesta e riceve la risposta.
  5. Viene eseguito il passaggio di test Assert : la risposta effettiva viene convalidata come passaggio o esito negativo in base a una risposta prevista .
  6. Il processo continua fino a quando non vengono eseguiti tutti i test.
  7. I risultati del test vengono segnalati.

In genere, l'host Web di test viene configurato in modo diverso rispetto al normale host Web dell'app per le esecuzioni di test. Ad esempio, per i test è possibile usare un database diverso o impostazioni di app diverse.

I componenti dell'infrastruttura, ad esempio l'host Web di test e il server di test in memoria (TestServer), vengono forniti o gestiti dal pacchetto Microsoft.AspNetCore.Mvc.Testing. L'uso di questo pacchetto semplifica la creazione e l'esecuzione dei test.

Il Microsoft.AspNetCore.Mvc.Testing pacchetto gestisce le attività seguenti:

  • Copia il file delle dipendenze (.deps) dal SUT nella directory del progetto di bin test.
  • Imposta la radice del contenuto sulla radice del progetto di SUT in modo che i file statici e le pagine/visualizzazioni vengano trovati quando vengono eseguiti i test.
  • Fornisce la classe WebApplicationFactory per semplificare il bootstrap del SUT con TestServer.

La documentazione degli unit test descrive come configurare un progetto di test e uno strumento di esecuzione dei test, oltre a istruzioni dettagliate su come eseguire test e consigli su come assegnare un nome ai test e alle classi di test.

Separare gli unit test dai test di integrazione in progetti diversi. Separazione dei test:

  • Assicura che i componenti di test dell'infrastruttura non siano inclusi accidentalmente negli unit test.
  • Consente il controllo su quale set di test vengono eseguiti.

Non esiste praticamente alcuna differenza tra la configurazione per i test delle app Pages e delle Razor app MVC. L'unica differenza consiste nel modo in cui vengono denominati i test. In un'app Razor Pages, i test degli endpoint di pagina sono in genere denominati dopo la classe del modello di pagina, IndexPageTests ad esempio per testare l'integrazione dei componenti per la pagina Indice. In un'app MVC, i test sono in genere organizzati in base alle classi controller e denominati dopo i controller di cui eseguono il test, HomeControllerTests ad esempio per testare l'integrazione dei componenti per il Home controller.

Testare i prerequisiti dell'app

Il progetto di test deve:

Questi prerequisiti possono essere visualizzati nell'app di esempio. Esaminare il file tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. L'app di esempio usa il framework di test xUnit e la libreria parser AngleSharp , quindi l'app di esempio fa riferimento anche a:

Nelle app che usano xunit.runner.visualstudio la versione 2.4.2 o successiva, il progetto di test deve fare riferimento al Microsoft.NET.Test.Sdk pacchetto.

Entity Framework Core viene usato anche nei test. Vedere il file di progetto in GitHub.

Ambiente SUT

Se l'ambiente di SUT non è impostato, per impostazione predefinita l'ambiente è Sviluppo.

Test di base con WebApplicationFactory predefinito

Esporre la classe definita Program in modo implicito al progetto di test eseguendo una delle operazioni seguenti:

  • Esporre i tipi interni dall'app Web al progetto di test. Questa operazione può essere eseguita nel file del progetto SUT (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Rendere pubblica la Program classe usando una dichiarazione di classe parziale:

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

    L'app di esempio usa l'approccio Program di classe parziale.

WebApplicationFactory<TEntryPoint> viene usato per creare un TestServer oggetto per i test di integrazione. TEntryPoint è la classe del punto di ingresso di SUT, in genere Program.cs.

Le classi di test implementano un'interfaccia della fixture di classe (IClassFixture) per indicare che la classe contiene test e fornire istanze di oggetti condivisi tra i test nella classe .

La classe di test seguente, BasicTests, usa per WebApplicationFactory eseguire il bootstrap dell'oggetto SUT e fornire un HttpClient oggetto a un metodo di test, Get_EndpointsReturnSuccessAndCorrectContentType. Il metodo verifica che il codice di stato della risposta sia riuscito (200-299) e l'intestazione Content-Type sia text/html; charset=utf-8 per diverse pagine dell'app.

CreateClient() crea un'istanza di HttpClient che segue automaticamente i reindirizzamenti e gestisce i 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());
    }
}

Per impostazione predefinita, i cookie non essenziali non vengono mantenuti tra le richieste quando sono abilitati i criteri di consenso del Regolamento generale sulla protezione dei dati. Per conservare i cookie non essenziali, ad esempio quelli usati dal provider TempData, contrassegnarli come essenziali nei test. Per istruzioni su come contrassegnare un oggetto cookie come essenziale, vedere Cookie essenziali.

AngleSharp e Application Parts per i controlli antiforgery

Questo articolo usa il parser AngleSharp per gestire i controlli antiforgery caricando le pagine e analizzando il codice HTML. Per testare gli endpoint delle visualizzazioni controller e Razor Pages a un livello inferiore, senza preoccuparsi del modo in cui eseguono il rendering nel browser, è consigliabile usare Application Parts. L'approccio Parti dell'applicazione inserisce un controller o Razor una pagina nell'app che può essere usata per effettuare richieste JSON per ottenere i valori necessari. Per altre informazioni, vedere il blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts (Test di integrazione ASP.NET Risorse principali protette con Antiforgery Using Application Parts ) e il repository GitHub associato di Martin Costello.

Personalizzare WebApplicationFactory

La configurazione dell'host Web può essere creata indipendentemente dalle classi di test ereditando da WebApplicationFactory<TEntryPoint> per creare una o più factory personalizzate:

  1. Ereditare da WebApplicationFactory ed eseguire l'override ConfigureWebHostdi . IWebHostBuilder consente la configurazione della raccolta di servizi conIWebHostBuilder.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");
        }
    }
    

    Il seeding del database nell'app di esempio viene eseguito dal InitializeDbForTests metodo . Il metodo è descritto nella sezione Esempio di test di integrazione: Testare l'organizzazione dell'app.

    Il contesto del database di SUT viene registrato in Program.cs. Il callback dell'app di builder.ConfigureServices test viene eseguito dopo l'esecuzione del codice dell'app Program.cs . Per usare un database diverso per i test rispetto al database dell'app, il contesto del database dell'app deve essere sostituito in builder.ConfigureServices.

    L'app di esempio trova il descrittore del servizio per il contesto del database e usa il descrittore per rimuovere la registrazione del servizio. La factory aggiunge quindi un nuovo ApplicationDbContext oggetto che usa un database in memoria per i test.

    Per connettersi a un database diverso, modificare .DbConnection Per usare un database di test di SQL Server:

  1. Usare l'oggetto personalizzato CustomWebApplicationFactory nelle classi di test. Nell'esempio seguente viene usata la factory nella IndexPageTests classe :

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

    Il client dell'app di esempio è configurato per impedire i HttpClient reindirizzamenti seguenti. Come spiegato più avanti nella sezione Autenticazione fittizia , questo consente ai test di controllare il risultato della prima risposta dell'app. La prima risposta è un reindirizzamento in molti di questi test con un'intestazione Location .

  2. Un test tipico usa i HttpClient metodi helper e per elaborare la richiesta e la risposta:

    [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);
    }
    

Qualsiasi richiesta POST al SUT deve soddisfare il controllo antiforgery effettuato automaticamente dal sistema antiforgery di protezione dei dati dell'app. Per organizzare la richiesta POST di un test, l'app di test deve:

  1. Effettuare una richiesta per la pagina.
  2. Analizzare l'antiforgery cookie e richiedere il token di convalida dalla risposta.
  3. Effettuare la richiesta POST con il token di convalida antiforgery cookie e richiesta sul posto.

I SendAsync metodi di estensione helper (Helpers/HttpClientExtensions.cs) e il GetDocumentAsync metodo helper (Helpers/HtmlHelpers.cs) nell'app di esempio usano il parser AngleSharp per gestire il controllo antiforgery con i metodi seguenti:

  • GetDocumentAsync: riceve e restituisce HttpResponseMessage un oggetto IHtmlDocument. GetDocumentAsync usa una factory che prepara una risposta virtuale basata sull'originale HttpResponseMessage. Per altre informazioni, vedere la documentazione di AngleSharp.
  • SendAsync metodi di estensione per la HttpClient composizione di un HttpRequestMessage oggetto e una chiamata SendAsync(HttpRequestMessage) per inviare richieste al SUT. Overload per SendAsync accettare il modulo HTML (IHtmlFormElement) e gli elementi seguenti:
    • Pulsante Invia del modulo (IHtmlElement)
    • Insieme di valori modulo (IEnumerable<KeyValuePair<string, string>>)
    • Pulsante Invia (IHtmlElement) e valori modulo (IEnumerable<KeyValuePair<string, string>>)

AngleSharp è una libreria di analisi di terze parti usata a scopo dimostrativo in questo articolo e nell'app di esempio. AngleSharp non è supportato o necessario per il test di integrazione di app ASP.NET Core. È possibile usare altri parser, ad esempio Html Agility Pack (HAP). Un altro approccio consiste nel scrivere codice per gestire direttamente il token di verifica delle richieste del sistema antiforgery e antiforgery cookie . Per altre informazioni, vedere AngleSharp vs Application Parts for antiforgery check in questo articolo.

Il provider di database EF-Core in memoria può essere usato per test limitati e di base, ma il provider SQLite è la scelta consigliata per i test in memoria.

Vedere Estendere l'avvio con filtri di avvio che illustrano come configurare il middleware usando IStartupFilter, utile quando un test richiede un servizio personalizzato o un middleware.

Personalizzare il client con WithWebHostBuilder

Quando è necessaria una configurazione aggiuntiva all'interno di un metodo di test, WithWebHostBuilder crea un nuovo WebApplicationFactory oggetto con un IWebHostBuilder oggetto ulteriormente personalizzato in base alla configurazione.

Il codice di esempio chiama WithWebHostBuilder per sostituire i servizi configurati con stub di test. Per altre informazioni e l'utilizzo di esempio, vedere Inserire servizi fittizi in questo articolo.

Il Post_DeleteMessageHandler_ReturnsRedirectToRoot metodo di test dell'app di esempio illustra l'uso di WithWebHostBuilder. Questo test esegue un'eliminazione di record nel database attivando un invio di modulo nel SUT.

Poiché un altro test nella IndexPageTests classe esegue un'operazione che elimina tutti i record nel database e può essere eseguito prima del Post_DeleteMessageHandler_ReturnsRedirectToRoot metodo , il database viene reinviato in questo metodo di test per assicurarsi che un record sia presente per l'eliminazione di SUT. La selezione del primo pulsante di eliminazione del messages modulo nel SUT viene simulata nella richiesta al SUT:

[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);
}

Opzioni client

Per le impostazioni predefinite e le opzioni disponibili durante la creazione di HttpClient istanze, vedere la WebApplicationFactoryClientOptions pagina.

Creare la WebApplicationFactoryClientOptions classe e passarla al CreateClient() metodo :

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

NOTA: per evitare avvisi di reindirizzamento HTTPS nei log quando si usa il middleware di reindirizzamento HTTPS, impostare BaseAddress = new Uri("https://localhost")

Inserire servizi fittizi

I servizi possono essere sottoposti a override in un test con una chiamata a ConfigureTestServices nel generatore host. Per definire l'ambito dei servizi sottoposti a override al test stesso, il WithWebHostBuilder metodo viene usato per recuperare un generatore di host. Questo problema può essere visualizzato nei test seguenti:

L'esempio SUT include un servizio con ambito che restituisce un'offerta. L'offerta è incorporata in un campo nascosto nella pagina Indice quando viene richiesta la pagina Indice.

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">

Quando viene eseguita l'app SUT, viene generato il markup seguente:

<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.">

Per testare il servizio e l'inserimento di virgolette in un test di integrazione, un servizio fittizio viene inserito nel SUT dal test. Il servizio fittizio sostituisce l'app QuoteService con un servizio fornito dall'app di test, denominato 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 viene chiamato e il servizio con ambito è registrato:

[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);
}

Il markup generato durante l'esecuzione del test riflette il testo delle virgolette fornito da TestQuoteService, quindi l'asserzione supera:

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

Autenticazione fittizia

I test nella AuthTests classe verificano che un endpoint sicuro:

  • Reindirizza un utente non autenticato alla pagina di accesso dell'app.
  • Restituisce il contenuto per un utente autenticato.

In SUT la /SecurePage pagina usa una AuthorizePage convenzione per applicare un oggetto AuthorizeFilter alla pagina. Per altre informazioni, vedere Razor Convenzioni di autorizzazione pagine.

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

Get_SecurePageRedirectsAnUnauthenticatedUser Nel test un WebApplicationFactoryClientOptions oggetto è impostato su non consentire i reindirizzamenti impostando AllowAutoRedirect su 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);
}

Non consentendo al client di seguire il reindirizzamento, è possibile effettuare i controlli seguenti:

  • Il codice di stato restituito da SUT può essere controllato rispetto al risultato previsto HttpStatusCode.Redirect , non al codice di stato finale dopo il reindirizzamento alla pagina di accesso, che sarebbe HttpStatusCode.OK.
  • Il Location valore dell'intestazione nelle intestazioni della risposta viene controllato per confermare che inizia con http://localhost/Identity/Account/Login, non la risposta finale della pagina di accesso, in cui l'intestazione Location non sarebbe presente.

L'app di test può simulare un oggetto AuthenticationHandler<TOptions> ConfigureTestServices per testare gli aspetti dell'autenticazione e dell'autorizzazione. Uno scenario minimo restituisce :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);
    }
}

Viene TestAuthHandler chiamato per autenticare un utente quando lo schema di autenticazione è impostato su TestScheme dove AddAuthentication è registrato per ConfigureTestServices. È importante che lo TestScheme schema corrisponda allo schema previsto dall'app. In caso contrario, l'autenticazione non funzionerà.

[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);
}

Per altre informazioni su WebApplicationFactoryClientOptions, vedere la sezione Opzioni client.

Test di base per il middleware di autenticazione

Vedere questo repository GitHub per i test di base del middleware di autenticazione. Contiene un server di test specifico dello scenario di test.

Impostare l'ambiente

Impostare l'ambiente nella factory dell'applicazione personalizzata:

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");
    }
}

Come l'infrastruttura di test deduce il percorso radice del contenuto dell'app

Il WebApplicationFactory costruttore deduce il percorso radice del contenuto dell'app cercando un WebApplicationFactoryContentRootAttribute nell'assembly contenente i test di integrazione con una chiave uguale all'assembly TEntryPoint System.Reflection.Assembly.FullName. Se non viene trovato un attributo con la chiave corretta, WebApplicationFactory esegue nuovamente la ricerca di un file di soluzione (.sln) e aggiunge il nome dell'assembly TEntryPoint alla directory della soluzione. La directory radice dell'app (percorso radice del contenuto) viene usata per individuare visualizzazioni e file di contenuto.

Disabilitare la copia shadow

La copia shadow fa sì che i test vengano eseguiti in una directory diversa rispetto alla directory di output. Se i test si basano sul caricamento di file relativi a Assembly.Location e si verificano problemi, potrebbe essere necessario disabilitare la copia shadow.

Per disabilitare la copia shadow quando si usa xUnit, creare un xunit.runner.json file nella directory del progetto di test con l'impostazione di configurazione corretta:

{
  "shadowCopy": false
}

Eliminazione di oggetti

Dopo l'esecuzione TestServer dei test dell'implementazione IClassFixture e HttpClient vengono eliminati quando xUnit elimina .WebApplicationFactory Se gli oggetti creati dallo sviluppatore richiedono l'eliminazione, eliminarli nell'implementazione IClassFixture . Per altre informazioni, vedere Implementazione di un metodo Dispose.

Esempio di test di integrazione

L'app di esempio è costituita da due app:

App Directory del progetto Descrizione
App messaggio (SUT) src/RazorPagesProject Consente a un utente di aggiungere, eliminare, eliminare tutti e analizzare i messaggi.
Testare l'app tests/RazorPagesProject.Tests Usato per eseguire il test dell'integrazione di SUT.

I test possono essere eseguiti usando le funzionalità di test predefinite di un IDE, ad esempio Visual Studio. Se si usa Visual Studio Code o la riga di comando, eseguire il comando seguente al prompt dei comandi nella tests/RazorPagesProject.Tests directory:

dotnet test

Organizzazione dell'app message (SUT)

SUT è un Razor sistema di messaggi Pages con le caratteristiche seguenti:

  • La pagina Index dell'app (Pages/Index.cshtml e Pages/Index.cshtml.cs) fornisce metodi di interfaccia utente e modello di pagina per controllare l'aggiunta, l'eliminazione e l'analisi dei messaggi (parole medie per messaggio).
  • Un messaggio viene descritto dalla Message classe (Data/Message.cs) con due proprietà: Id (chiave) e Text (messaggio). La Text proprietà è obbligatoria e limitata a 200 caratteri.
  • I messaggi vengono archiviati usando il database in memoria di Entity Framework†.
  • L'app contiene un livello di accesso ai dati (DAL) nella classe di contesto del database (AppDbContextData/AppDbContext.cs).
  • Se il database è vuoto all'avvio dell'app, l'archivio messaggi viene inizializzato con tre messaggi.
  • L'app include un oggetto /SecurePage accessibile solo da un utente autenticato.

†Il test di Entity Framework con InMemory illustra come usare un database in memoria per i test con MSTest. Questo argomento usa il framework di test xUnit . I concetti di test e le implementazioni di test in framework di test diversi sono simili ma non identici.

Anche se l'app non usa il modello di repository e non è un esempio efficace del modello Unit of Work (UoW), Razor Pages supporta questi modelli di sviluppo. Per altre informazioni, vedere Progettazione del livello di persistenza dell'infrastruttura e Logica del controller di test (l'esempio implementa il modello di repository).

Testare l'organizzazione dell'app

L'app di test è un'app console all'interno della tests/RazorPagesProject.Tests directory.

Testare la directory dell'app Descrizione
AuthTests Contiene i metodi di test per:
  • Accesso a una pagina protetta da un utente non autenticato.
  • Accesso a una pagina sicura da parte di un utente autenticato con un fittizio AuthenticationHandler<TOptions>.
  • Ottenere un profilo utente GitHub e controllare l'account di accesso utente del profilo.
BasicTests Contiene un metodo di test per il routing e il tipo di contenuto.
IntegrationTests Contiene i test di integrazione per la pagina Index usando la classe personalizzata WebApplicationFactory .
Helpers/Utilities
  • Utilities.cs contiene il metodo utilizzato per eseguire il InitializeDbForTests seeding del database con i dati di test.
  • HtmlHelpers.cs fornisce un metodo per restituire un AngleSharp IHtmlDocument da utilizzare dai metodi di test.
  • HttpClientExtensions.cs fornire overload per SendAsync inviare richieste al SUT.

Il framework di test è xUnit. I test di integrazione vengono eseguiti usando , Microsoft.AspNetCore.TestHostche include .TestServer Poiché il Microsoft.AspNetCore.Mvc.Testing pacchetto viene usato per configurare l'host di test e il server di test, i TestHost pacchetti e TestServer non richiedono riferimenti diretti al pacchetto nel file di progetto o nella configurazione dello sviluppatore dell'app di test nell'app di test.

I test di integrazione richiedono in genere un set di dati di piccole dimensioni nel database prima dell'esecuzione del test. Ad esempio, una chiamata di test di eliminazione per l'eliminazione di un record di database, pertanto il database deve avere almeno un record affinché la richiesta di eliminazione abbia esito positivo.

L'app di esempio esegue il seeding del database con tre messaggi in Utilities.cs che i test possono usare quando vengono eseguiti:

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." }
    };
}

Il contesto del database di SUT viene registrato in Program.cs. Il callback dell'app di builder.ConfigureServices test viene eseguito dopo l'esecuzione del codice dell'app Program.cs . Per usare un database diverso per i test, il contesto del database dell'app deve essere sostituito in builder.ConfigureServices. Per altre informazioni, vedere la sezione Customize WebApplicationFactory .

Risorse aggiuntive