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 :
- L'host Web SUT è configurato.
- Viene creato un client del server di test per inviare richieste all'app.
- Viene eseguito il passaggio Disponi test: l'app di test prepara una richiesta.
- Viene eseguito il passaggio di test act : il client invia la richiesta e riceve la risposta.
- Viene eseguito il passaggio di test Assert : la risposta effettiva viene convalidata come passaggio o esito negativo in base a una risposta prevista .
- Il processo continua fino a quando non vengono eseguiti tutti i test.
- 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 dibin
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:
- Fare riferimento al pacchetto
Microsoft.AspNetCore.Mvc.Testing
. - Specificare Web SDK nel file di progetto (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
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:
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 dibuilder.ConfigureServices
test viene eseguito dopo l'esecuzione del codice dell'appProgram.cs
. Per usare un database diverso per i test rispetto al database dell'app, il contesto del database dell'app deve essere sostituito inbuilder.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:- Fare riferimento al
Microsoft.EntityFrameworkCore.SqlServer
pacchetto NuGet nel file di progetto. - Chiamare
UseInMemoryDatabase
.
- Fare riferimento al
Usare l'oggetto personalizzato
CustomWebApplicationFactory
nelle classi di test. Nell'esempio seguente viene usata la factory nellaIndexPageTests
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'intestazioneLocation
.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:
- Effettuare una richiesta per la pagina.
- Analizzare l'antiforgery cookie e richiedere il token di convalida dalla risposta.
- 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 oggettoIHtmlDocument
.GetDocumentAsync
usa una factory che prepara una risposta virtuale basata sull'originaleHttpResponseMessage
. Per altre informazioni, vedere la documentazione di AngleSharp.SendAsync
metodi di estensione per laHttpClient
composizione di un HttpRequestMessage oggetto e una chiamata SendAsync(HttpRequestMessage) per inviare richieste al SUT. Overload perSendAsync
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>>
)
- Pulsante Invia del modulo (
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:
- Get_QuoteService_ProvidesQuoteInPage
- Get_GithubProfilePageCanGetAGithubUser
- Get_SecurePageIsReturnedForAnAuthenticatedUser
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've an appointment in
London, and we'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'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 conhttp://localhost/Identity/Account/Login
, non la risposta finale della pagina di accesso, in cui l'intestazioneLocation
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
ePages/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) eText
(messaggio). LaText
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 (
AppDbContext
Data/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:
|
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 |
|
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 :
- L'host Web SUT è configurato.
- Viene creato un client del server di test per inviare richieste all'app.
- Viene eseguito il passaggio Disponi test: l'app di test prepara una richiesta.
- Viene eseguito il passaggio di test act : il client invia la richiesta e riceve la risposta.
- Viene eseguito il passaggio di test Assert : la risposta effettiva viene convalidata come passaggio o esito negativo in base a una risposta prevista .
- Il processo continua fino a quando non vengono eseguiti tutti i test.
- 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 dibin
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:
- Fare riferimento al pacchetto
Microsoft.AspNetCore.Mvc.Testing
. - Specificare Web SDK nel file di progetto (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
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:
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.InMemory
Microsoft.EntityFrameworkCore.Tools
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:
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 dibuilder.ConfigureServices
test viene eseguito dopo l'esecuzione del codice dell'appStartup.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 inbuilder.ConfigureServices
.Per i sut che usano ancora l'host Web, il callback dell'app di
builder.ConfigureServices
test viene eseguito prima del codice diStartup.ConfigureServices
SUT. Il callback dell'app dibuilder.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:- Fare riferimento al
Microsoft.EntityFrameworkCore.SqlServer
pacchetto NuGet nel file di progetto. - Chiamare
UseSqlServer
con un stringa di connessione al database.
services.AddDbContext<ApplicationDbContext>((options, context) => { context.UseSqlServer( Configuration.GetConnectionString("TestingDbConnectionString")); });
- Fare riferimento al
Usare l'oggetto personalizzato
CustomWebApplicationFactory
nelle classi di test. Nell'esempio seguente viene usata la factory nellaIndexPageTests
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'intestazioneLocation
.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:
- Effettuare una richiesta per la pagina.
- Analizzare l'antiforgery cookie e richiedere il token di convalida dalla risposta.
- 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 oggettoIHtmlDocument
.GetDocumentAsync
usa una factory che prepara una risposta virtuale basata sull'originaleHttpResponseMessage
. Per altre informazioni, vedere la documentazione di AngleSharp.SendAsync
metodi di estensione per laHttpClient
composizione di un HttpRequestMessage oggetto e una chiamata SendAsync(HttpRequestMessage) per inviare richieste al SUT. Overload perSendAsync
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>>
)
- Pulsante Invia del modulo (
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've an appointment in
London, and we'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'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 conhttp://localhost/Identity/Account/Login
, non la risposta finale della pagina di accesso, in cui l'intestazioneLocation
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 ,Staging
Production
o un altro valore personalizzato, ad esempioTesting
. - Eseguire l'override nell'app di test per leggere le variabili di ambiente precedute
CreateHostBuilder
daASPNETCORE
.
protected override IHostBuilder CreateHostBuilder() =>
base.CreateHostBuilder()
.ConfigureHostConfiguration(
config => config.AddEnvironmentVariables("ASPNETCORE"));
Se suT usa l'host Web (IWebHostBuilder
), eseguire l'override CreateWebHostBuilder
di :
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
ePages/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) eText
(messaggio). LaText
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 (
AppDbContext
Data/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:
|
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 |
|
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 :
- L'host Web SUT è configurato.
- Viene creato un client del server di test per inviare richieste all'app.
- Viene eseguito il passaggio Disponi test: l'app di test prepara una richiesta.
- Viene eseguito il passaggio di test act : il client invia la richiesta e riceve la risposta.
- Viene eseguito il passaggio di test Assert : la risposta effettiva viene convalidata come passaggio o esito negativo in base a una risposta prevista .
- Il processo continua fino a quando non vengono eseguiti tutti i test.
- 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 dibin
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:
- Fare riferimento al pacchetto
Microsoft.AspNetCore.Mvc.Testing
. - Specificare Web SDK nel file di progetto (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
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:
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 dibuilder.ConfigureServices
test viene eseguito dopo l'esecuzione del codice dell'appProgram.cs
. Per usare un database diverso per i test rispetto al database dell'app, il contesto del database dell'app deve essere sostituito inbuilder.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:
- Fare riferimento al
Microsoft.EntityFrameworkCore.SqlServer
pacchetto NuGet nel file di progetto. - Chiamare
UseInMemoryDatabase
.
Usare l'oggetto personalizzato
CustomWebApplicationFactory
nelle classi di test. Nell'esempio seguente viene usata la factory nellaIndexPageTests
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'intestazioneLocation
.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:
- Effettuare una richiesta per la pagina.
- Analizzare l'antiforgery cookie e richiedere il token di convalida dalla risposta.
- 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 oggettoIHtmlDocument
.GetDocumentAsync
usa una factory che prepara una risposta virtuale basata sull'originaleHttpResponseMessage
. Per altre informazioni, vedere la documentazione di AngleSharp.SendAsync
metodi di estensione per laHttpClient
composizione di un HttpRequestMessage oggetto e una chiamata SendAsync(HttpRequestMessage) per inviare richieste al SUT. Overload perSendAsync
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>>
)
- Pulsante Invia del modulo (
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:
- Get_QuoteService_ProvidesQuoteInPage
- Get_GithubProfilePageCanGetAGithubUser
- Get_SecurePageIsReturnedForAnAuthenticatedUser
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've an appointment in
London, and we'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'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 conhttp://localhost/Identity/Account/Login
, non la risposta finale della pagina di accesso, in cui l'intestazioneLocation
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
ePages/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) eText
(messaggio). LaText
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 (
AppDbContext
Data/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:
|
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 |
|
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 .