Test di integrazione in ASP.NET Core
Di Jos van der Til, Martin Costello e Javier 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 SPA, consigliamo 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:
- base di dati
- Sistema di gestione dei file
- 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 del SUT è configurato.
- Viene creato un client del server di test per inviare richieste all'app.
- Il Passaggio Disponi viene eseguito: l'applicazione 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 di test del progettobin
. - Imposta la root del content alla root del progetto di SUT, così che i file statici e le pagine/view siano trovati durante l'esecuzione dei 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 la versione xunit.runner.visualstudio
2.4.2 o successiva, il progetto di test deve fare riferimento al pacchetto Microsoft.NET.Test.Sdk
.
Entity Framework Core viene usato anche nei test. Vedi il file di progetto su GitHub.
Ambiente SUT
Se l'ambiente del SUT non è impostato, l'ambiente predefinito è Sviluppo.
Test di base con WebApplicationFactory predefinito
WebApplicationFactory<TEntryPoint> viene usato per creare un TestServer 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 fixture di classe () per indicare che contengono test e per fornire istanze di oggetti condivisi tra i test all'interno della classe.
La seguente classe di test, BasicTests
, utilizza WebApplicationFactory
per eseguire il bootstrap del SUT e fornire un HttpClient a un metodo di test, Get_EndpointsReturnSuccessAndCorrectContentType
. Il metodo verifica che il codice di stato della risposta sia compreso tra (200-299) e che l'intestazione Content-Type
sia text/html; charset=utf-8
per varie 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 contro 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 del controller e delle pagine Razor a un livello più basso, senza preoccuparsi di come vengono eseguiti 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:
Eredita da
WebApplicationFactory
e sovrascrivi ConfigureWebHost. 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(IDbContextOptionsConfiguration<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 attraverso il metodo
InitializeDbForTests
. Il metodo è descritto nella sezione Esempio di test di integrazione: Testare l'organizzazione dell'app.Il contesto del database di SUT viene registrato in
Program.cs
. Il callback dell'app di testbuilder.ConfigureServices
viene eseguito dopo che è stato eseguito il 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 il
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 il 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 utilizza
HttpClient
e i metodi helper 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 di protezione dei dati antiforgery 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.
- Esegui la richiesta POST con il token di convalida e antifalsificazione cookie incluso.
I SendAsync
metodi di estensione helper (Helpers/HttpClientExtensions.cs
) e il GetDocumentAsync
metodo helper (Helpers/HtmlHelpers.cs
) nell'applicazione di esempio usano il parser AngleSharp per gestire il controllo antiforgery con i metodi seguenti:
-
GetDocumentAsync
: riceve HttpResponseMessage e restituisce un oggettoIHtmlDocument
.GetDocumentAsync
utilizza una fabbrica che prepara una risposta virtuale basata sull'originaleHttpResponseMessage
. Per altre informazioni, vedere la documentazione di AngleSharp. -
SendAsync
metodi di estensione perHttpClient
compongono un HttpRequestMessage e chiamano SendAsync(HttpRequestMessage) per inviare richieste al SUT. Overload perSendAsync
accettano il modulo HTML (IHtmlFormElement
) e gli elementi seguenti:- Pulsante Invia del modulo (
IHtmlElement
) - Raccolta di valori del modulo (
IEnumerable<KeyValuePair<string, string>>
) - Pulsante di invio (
IHtmlElement
) e valori del 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 nello scrivere codice per gestire direttamente il token di verifica delle richieste del sistema antiforgery e il componente antiforgery cookie. Per ulteriori informazioni, vedere AngleSharp vs Application Parts
per i controlli antifalsificazione 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.
Vedi Estendere il processo di avvio con filtri di avvio, che mostra come configurare il middleware usando IStartupFilter, utile quando un test richiede un servizio o un middleware personalizzato.
Personalizzare il client con WithWebHostBuilder
Quando è necessaria una configurazione aggiuntiva all'interno di un metodo di test, WithWebHostBuilder crea un nuovo WebApplicationFactory
con un IWebHostBuilder 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 WebApplicationFactoryClientOptions istanze, vedere la HttpClient
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 simulati
I servizi possono essere sovrascritti in un test con una chiamata a ConfigureTestServices nel generatore host. Per definire l'ambito dei servizi sottoposti a override nel test stesso, il metodo WithWebHostBuilder viene usato per recuperare un generatore di host. Questo problema può essere visualizzato nei test seguenti:
- Get_QuoteService_ProvidesQuoteInPage
- Get_GithubProfilePageCanGetAGithubUser
- Ottieni_PaginaSicuraRestituitaPerUtenteAutenticato
L'esempio SUT include un servizio con ambito limitato che restituisce un preventivo. La citazione è incorporata in un campo nascosto nella pagina Indice quando questa viene richiesta.
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 a scopo è 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 della citazione fornito da TestQuoteService
, quindi l'asserzione viene superata.
<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.
Nella SUT, la pagina /SecurePage
utilizza una convenzione AuthorizePage per applicare un AuthorizeFilter alla pagina. Per ulteriori informazioni, vedere Razor convenzioni di autorizzazione delle pagine.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Get_SecurePageRedirectsAnUnauthenticatedUser
Nel test, un WebApplicationFactoryClientOptions è impostato per 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 valore dell'intestazione
Location
nelle intestazioni della risposta viene controllato per confermare che inizia conhttp://localhost/Identity/Account/Login
, anziché 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 delle opzioni del client.
Test di base per il middleware di autenticazione
Consulta 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
Imposta 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(IDbContextOptionsConfiguration<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 costruttore deduce il percorso radice del contenuto dell'app cercando un elemento WebApplicationFactory
nell'assembly che contiene i test di integrazione, con una chiave uguale a quella dell'assembly WebApplicationFactoryContentRootAttribute. 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 delle ombre
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 la corretta impostazione di configurazione:
{
"shadowCopy": false
}
Eliminazione di oggetti
Dopo l'esecuzione dei test dell'implementazione IClassFixture
, TestServer 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:
Applicazione | Directory del progetto | Descrizione |
---|---|---|
App di messaggistica (SUT) | src/RazorPagesProject |
Consente a un utente di aggiungere, eliminare, eliminare tutti e analizzare i messaggi. |
Testare l'app | tests/RazorPagesProject.Tests |
Usato per il test di integrazione del 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 Messaggi (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.
†L'articolo di Entity Framework, Test con InMemory, spiega 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 utilizzando il Microsoft.AspNetCore.TestHost, che include il 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, un test di eliminazione, che richiede l'eliminazione di un record nel 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 test builder.ConfigureServices
viene eseguito dopo che è stato eseguito il 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 (SPA), si consiglia di utilizzare 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:
- base di dati
- Sistema di gestione dei file
- 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 del SUT è configurato.
- Viene creato un client del server di test per inviare richieste all'app.
- Il Passaggio Disponi viene eseguito: l'applicazione 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 di test del progettobin
. - Imposta la root del content alla root del progetto di SUT, così che i file statici e le pagine/view siano trovati durante l'esecuzione dei 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 la versione xunit.runner.visualstudio
2.4.2 o successiva, il progetto di test deve fare riferimento al pacchetto Microsoft.NET.Test.Sdk
.
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 del SUT non è impostato, l'ambiente predefinito è Sviluppo.
Test di base con WebApplicationFactory predefinito
WebApplicationFactory<TEntryPoint> viene usato per creare un TestServer 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 fixture di classe () per indicare che contengono test e per fornire istanze di oggetti condivisi tra i test all'interno della classe.
La seguente classe di test, BasicTests
, utilizza WebApplicationFactory
per eseguire il bootstrap del SUT e fornire un HttpClient 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 conservati tra le richieste quando la politica di consenso GDPR è abilitata. 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 il WebApplicationFactory
La configurazione dell'host Web può essere creata indipendentemente dalle classi di test ereditando da WebApplicationFactory
per creare una o più factory personalizzate:
Eredita da
WebApplicationFactory
e sovrascrivi ConfigureWebHost. 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 attraverso il metodo
InitializeDbForTests
. Il metodo è descritto nella sezione Esempio di test di integrazione: Testare l'organizzazione dell'app.Il contesto del database di SUT viene registrato nel relativo
Startup.ConfigureServices
metodo. Il callback dell'app di testbuilder.ConfigureServices
viene eseguito dopo che è stato eseguito il codice dell'appStartup.ConfigureServices
. L'ordine di esecuzione è una modifica importante per il Generic Host 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 delStartup.ConfigureServices
SUT. Il callback dell'appbuilder.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 il 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 utilizza
HttpClient
e i metodi helper 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 che viene effettuato automaticamente dal sistema di protezione dei dati antiforgery 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.
- Esegui la richiesta POST con il token di convalida e antifalsificazione cookie incluso.
I
-
GetDocumentAsync
: riceve HttpResponseMessage e restituisce un oggettoIHtmlDocument
.GetDocumentAsync
utilizza una fabbrica che prepara una risposta virtuale basata sull'originaleHttpResponseMessage
. Per altre informazioni, vedere la documentazione di AngleSharp. -
SendAsync
metodi di estensione perHttpClient
compongono un HttpRequestMessage e chiamano SendAsync(HttpRequestMessage) per inviare richieste al SUT. Overload perSendAsync
accettano il modulo HTML (IHtmlFormElement
) e gli elementi seguenti:- Pulsante di invio del modulo (
IHtmlElement
) - Raccolta di valori del modulo (
IEnumerable<KeyValuePair<string, string>>
) - Pulsante di invio (
IHtmlElement
) e valori del modulo (IEnumerable<KeyValuePair<string, string>>
)
- Pulsante di invio 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 nello scrivere codice per gestire direttamente il token di verifica delle richieste del sistema antiforgery e il componente antiforgery cookie.
Nota
Il provider di database EF-Core in memoria può essere utilizzato per test limitati e di base; tuttavia, 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
con un IWebHostBuilder 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 | Predefinito |
---|---|---|
AllowAutoRedirect | Ottieni o imposti se le istanze di HttpClient debbano 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 | Determina o ottiene il numero massimo di risposte di reindirizzamento che le istanze HttpClient devono seguire. |
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 simulati
I servizi possono essere sovrascritti 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 a contesto definito che restituisce un preventivo. La citazione è incorporata in un campo nascosto nella pagina Indice quando questa viene richiesta.
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 dati in un test di integrazione, un servizio fittizio viene iniettato 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 a scopo è 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.
Nella SUT, la pagina /SecurePage
utilizza una convenzione AuthorizePage per applicare un AuthorizeFilter alla pagina. Per ulteriori informazioni, vedere Razor convenzioni di autorizzazione delle pagine.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Get_SecurePageRedirectsAnUnauthenticatedUser
Nel test, un WebApplicationFactoryClientOptions è impostato per 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 valore dell'intestazione
Location
nelle intestazioni di risposta viene controllato per confermare che inizi conhttp://localhost/Identity/Account/Login
, e non con la risposta finale della pagina di accesso, dove 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 delle opzioni del 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
. - Sovrascrivere
CreateHostBuilder
nell'app di test per leggere variabili di ambiente prefissate conASPNETCORE
.
protected override IHostBuilder CreateHostBuilder() =>
base.CreateHostBuilder()
.ConfigureHostConfiguration(
config => config.AddEnvironmentVariables("ASPNETCORE"));
Se il SUT utilizza l'host Web (IWebHostBuilder
), sovrascrivi CreateWebHostBuilder
.
protected override IWebHostBuilder CreateWebHostBuilder() =>
base.CreateWebHostBuilder().UseEnvironment("Testing");
Come l'infrastruttura di test deduce il percorso radice del contenuto dell'app
Il costruttore deduce il percorso radice del contenuto dell'app cercando un elemento WebApplicationFactory
nell'assembly che contiene i test di integrazione, con una chiave uguale a quella dell'assembly WebApplicationFactoryContentRootAttribute. 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 delle ombre
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 la corretta impostazione di configurazione:
{
"shadowCopy": false
}
Eliminazione di oggetti
Dopo l'esecuzione dei test dell'implementazione IClassFixture
, TestServer 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:
Applicazione | Directory del progetto | Descrizione |
---|---|---|
App Messaggi (SUT) | src/RazorPagesProject |
Consente a un utente di aggiungere, eliminare, eliminare tutti e analizzare i messaggi. |
Testare l'app | tests/RazorPagesProject.Tests |
Usato per il test di integrazione del 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 Messaggi (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.
†L'argomento di Entity Framework, Test con InMemory, spiega come utilizzare 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 utilizzando il Microsoft.AspNetCore.TestHost, che include il 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, un test di eliminazione, che richiede l'eliminazione di un record nel 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 del SUT viene registrato nel suo metodo Startup.ConfigureServices
. Il callback dell'app di test builder.ConfigureServices
viene eseguito dopo che è stato eseguito il 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 del Startup.ConfigureServices
SUT. Il callback dell'app 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 SPA, consigliamo 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:
- base di dati
- Sistema di gestione dei file
- 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 del SUT è configurato.
- Viene creato un client del server di test per inviare richieste all'app.
- Il Passaggio Disponi viene eseguito: l'applicazione 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 di test del progettobin
. - Imposta la root del content alla root del progetto di SUT, così che i file statici e le pagine/view siano trovati durante l'esecuzione dei 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 la versione xunit.runner.visualstudio
2.4.2 o successiva, il progetto di test deve fare riferimento al pacchetto Microsoft.NET.Test.Sdk
.
Entity Framework Core viene usato anche nei test. Vedi il file del progetto su GitHub.
Ambiente SUT
Se l'ambiente del SUT non è impostato, l'ambiente predefinito è 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 dell'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 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 fixture di classe () per indicare che contengono test e per fornire istanze di oggetti condivisi tra i test all'interno della classe.
La seguente classe di test, BasicTests
, utilizza WebApplicationFactory
per eseguire il bootstrap del SUT e fornire un HttpClient a un metodo di test, Get_EndpointsReturnSuccessAndCorrectContentType
. Il metodo verifica che il codice di stato della risposta sia compreso tra (200-299) e che l'intestazione Content-Type
sia text/html; charset=utf-8
per varie 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 contro 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 del controller e delle pagine Razor a un livello più basso, senza preoccuparsi di come vengono eseguiti 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:
Eredita da
WebApplicationFactory
e sovrascrivi ConfigureWebHost. 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 attraverso il metodo
InitializeDbForTests
. Il metodo è descritto nella sezione Esempio di test di integrazione: Testare l'organizzazione dell'app.Il contesto del database di SUT viene registrato in
Program.cs
. Il callback dell'app di testbuilder.ConfigureServices
viene eseguito dopo che è stato eseguito il 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 fabbrica aggiunge quindi un nuovo
ApplicationDbContext
che utilizza un database in memoria per i test.Per connettersi a un database diverso, modificare il
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 il 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 utilizza
HttpClient
e i metodi helper 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 di protezione dei dati antiforgery dell'app. Per organizzare la richiesta POST di un test, l'app di test deve:
- Effettuare una richiesta per la pagina.
- Esaminare l'antiforgery cookie ed ottenere il token di convalida dalla risposta.
- Esegui la richiesta POST con il token di convalida e antifalsificazione cookie incluso.
I SendAsync
metodi di estensione helper (Helpers/HttpClientExtensions.cs
) e il GetDocumentAsync
metodo helper (Helpers/HtmlHelpers.cs
) nell'applicazione di esempio usano il parser di AngleSharp per gestire il controllo antiforgery con i metodi seguenti:
-
GetDocumentAsync
: riceve HttpResponseMessage e restituisce un oggettoIHtmlDocument
.GetDocumentAsync
utilizza una fabbrica che prepara una risposta virtuale basata sull'originaleHttpResponseMessage
. Per altre informazioni, vedere la documentazione di AngleSharp. -
SendAsync
metodi di estensione perHttpClient
compongono un HttpRequestMessage e chiamano SendAsync(HttpRequestMessage) per inviare richieste al SUT. Overload perSendAsync
accettano il modulo HTML (IHtmlFormElement
) e gli elementi seguenti:- Il pulsante "Invia" del modulo (
IHtmlElement
) - Raccolta di valori del modulo (
IEnumerable<KeyValuePair<string, string>>
) - Pulsante di invio (
IHtmlElement
) e valori del modulo (IEnumerable<KeyValuePair<string, string>>
)
- Il 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 nello scrivere codice per gestire direttamente il token di verifica delle richieste del sistema antiforgery e il componente antiforgery cookie. Per ulteriori informazioni, vedere AngleSharp vs Application Parts
per i controlli antifalsificazione in questo articolo.
Il provider di database EF-Core in memoria può essere usato per test di base e limitati, tuttavia il provider SQLite è la scelta consigliata per i test in memoria.
Vedi Estendere il processo di avvio con filtri di avvio, che mostra come configurare il middleware usando IStartupFilter, utile quando un test richiede un servizio o un middleware personalizzato.
Personalizzare il client con WithWebHostBuilder
Quando è necessaria una configurazione aggiuntiva all'interno di un metodo di test, WithWebHostBuilder crea un nuovo WebApplicationFactory
con un IWebHostBuilder 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 WebApplicationFactoryClientOptions istanze, vedere la HttpClient
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 simulati
I servizi possono essere sovrascritti 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
- Ottieni_PaginaSicuraRestituitaPerUtenteAutenticato
L'esempio SUT include un servizio con ambito definito che restituisce una citazione. La citazione è incorporata in un campo nascosto nella pagina Indice quando questa viene richiesta.
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'iniezione di codice in un test di integrazione, un servizio simulato viene iniettato nel SUT durante il 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 a scopo è 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 citato fornito da TestQuoteService
, quindi l'asserzione ha successo.
<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.
Nella SUT, la pagina /SecurePage
utilizza una convenzione AuthorizePage per applicare un AuthorizeFilter alla pagina. Per ulteriori informazioni, vedere Razor convenzioni di autorizzazione delle pagine.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Get_SecurePageRedirectsAnUnauthenticatedUser
Nel test, un WebApplicationFactoryClientOptions è impostato per 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 valore dell'intestazione
Location
nelle intestazioni della risposta viene controllato per confermare che inizia conhttp://localhost/Identity/Account/Login
, anziché 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 delle opzioni del client.
Test di base per il middleware di autenticazione
Consulta 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 fabbrica di applicazioni personalizzate.
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 costruttore deduce il percorso radice del contenuto dell'app cercando un elemento WebApplicationFactory
nell'assembly che contiene i test di integrazione, con una chiave uguale a quella dell'assembly WebApplicationFactoryContentRootAttribute. 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 delle ombre
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 la corretta impostazione di configurazione:
{
"shadowCopy": false
}
Eliminazione di oggetti
Dopo l'esecuzione dei test dell'implementazione IClassFixture
, TestServer e HttpClient vengono eliminati quando xUnit elimina WebApplicationFactory
. Se gli oggetti creati dallo sviluppatore richiedono l'eliminazione, eliminarli nell'implementazione IClassFixture
. Per ulteriori informazioni, vedere Implementazione di un metodo Dispose.
Esempio di test di integrazione
L'app di esempio è costituita da due app:
Applicazione | Directory del progetto | Descrizione |
---|---|---|
App di messaggistica (SUT) | src/RazorPagesProject |
Consente a un utente di aggiungere, eliminare, eliminare tutti e analizzare i messaggi. |
Testare l'app | tests/RazorPagesProject.Tests |
Usato per il test di integrazione del 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 Messaggi (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.
†L'articolo di Entity Framework, Test con InMemory, spiega 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 utilizzando il Microsoft.AspNetCore.TestHost, che include il 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, un test di eliminazione, che richiede l'eliminazione di un record nel 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 test builder.ConfigureServices
viene eseguito dopo che è stato eseguito il 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
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 SPA, consigliamo 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:
- base di dati
- Sistema di gestione dei file
- 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 del SUT è configurato.
- Viene creato un client del server di test per inviare richieste all'app.
- Il Passaggio Disponi viene eseguito: l'applicazione 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 di test del progettobin
. - Imposta la root del content alla root del progetto di SUT, così che i file statici e le pagine/view siano trovati durante l'esecuzione dei 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 la versione xunit.runner.visualstudio
2.4.2 o successiva, il progetto di test deve fare riferimento al pacchetto Microsoft.NET.Test.Sdk
.
Entity Framework Core viene usato anche nei test. Vedere il file di progetto su GitHub.
Ambiente SUT
Se l'ambiente del SUT non è impostato, l'ambiente predefinito è 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 dell'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 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 fixture di classe () per indicare che contengono test e per fornire istanze di oggetti condivisi tra i test all'interno della classe.
La seguente classe di test, BasicTests
, utilizza WebApplicationFactory
per eseguire il bootstrap del SUT e fornire un HttpClient a un metodo di test, Get_EndpointsReturnSuccessAndCorrectContentType
. Il metodo verifica che il codice di stato della risposta sia compreso tra (200-299) e che l'intestazione Content-Type
sia text/html; charset=utf-8
per varie 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 contro 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 del controller e delle pagine Razor a un livello più basso, senza preoccuparsi di come vengono eseguiti 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:
Eredita da
WebApplicationFactory
e sovrascrivi ConfigureWebHost. 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 attraverso il metodo
InitializeDbForTests
. Il metodo è descritto nella sezione Esempio di test di integrazione: Testare l'organizzazione dell'app.Il contesto del database di SUT viene registrato in
Program.cs
. Il callback dell'app di testbuilder.ConfigureServices
viene eseguito dopo che è stato eseguito il 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 il
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 il 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 utilizza
HttpClient
e i metodi helper 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 superare il controllo antiforgery, che viene eseguito automaticamente dal sistema di protezione dei dati antiforgery 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.
- Esegui la richiesta POST con il token di convalida e antifalsificazione cookie incluso.
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 HttpResponseMessage e restituisce un oggettoIHtmlDocument
.GetDocumentAsync
utilizza una fabbrica che prepara una risposta virtuale basata sull'originaleHttpResponseMessage
. Per altre informazioni, vedere la documentazione di AngleSharp. -
SendAsync
metodi di estensione perHttpClient
compongono un HttpRequestMessage e chiamano SendAsync(HttpRequestMessage) per inviare richieste al SUT. Overload perSendAsync
accettano il modulo HTML (IHtmlFormElement
) e gli elementi seguenti:- Pulsante di invio del modulo (
IHtmlElement
) - Raccolta di valori del modulo (
IEnumerable<KeyValuePair<string, string>>
) - Pulsante di invio (
IHtmlElement
) e valori del modulo (IEnumerable<KeyValuePair<string, string>>
)
- Pulsante di invio 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 nello scrivere codice per gestire direttamente il token di verifica delle richieste del sistema antiforgery e il componente antiforgery cookie. Per ulteriori informazioni, vedere AngleSharp vs Application Parts
per i controlli antifalsificazione in questo articolo.
Il provider di database EF-Core in memoria può essere usato per test limitati e di base; tuttavia, il provider SQLite è la scelta consigliata per i test in memoria.
Vedi Estendere il processo di avvio con filtri di avvio, che mostra come configurare il middleware usando IStartupFilter, utile quando un test richiede un servizio o un middleware personalizzato.
Personalizzare il client con WithWebHostBuilder
Quando è necessaria una configurazione aggiuntiva all'interno di un metodo di test, WithWebHostBuilder crea un nuovo WebApplicationFactory
con un IWebHostBuilder 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 WebApplicationFactoryClientOptions istanze, vedere la HttpClient
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 simulati
I servizi possono essere sovrascritti 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
- Ottieni_PaginaSicuraRestituitaPerUtenteAutenticato
L'esempio SUT include un servizio con ambito che restituisce un'offerta. La citazione è incorporata in un campo nascosto nella pagina Indice quando questa viene richiesta.
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 a scopo è 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 della citazione fornito da TestQuoteService
, quindi l'asserzione è superata.
<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.
Nella SUT, la pagina /SecurePage
utilizza una convenzione AuthorizePage per applicare un AuthorizeFilter alla pagina. Per ulteriori informazioni, vedere Razor convenzioni di autorizzazione delle pagine.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Get_SecurePageRedirectsAnUnauthenticatedUser
Nel test, un WebApplicationFactoryClientOptions è impostato per 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 valore dell'intestazione
Location
nelle intestazioni della risposta viene controllato per confermare che inizia conhttp://localhost/Identity/Account/Login
, anziché 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 delle opzioni del client.
Test di base per il middleware di autenticazione
Consulta 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 costruttore deduce il percorso radice del contenuto dell'app cercando un elemento WebApplicationFactory
nell'assembly che contiene i test di integrazione, con una chiave uguale a quella dell'assembly WebApplicationFactoryContentRootAttribute. 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 delle ombre
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 la corretta impostazione di configurazione:
{
"shadowCopy": false
}
Eliminazione di oggetti
Dopo l'esecuzione dei test dell'implementazione IClassFixture
, TestServer e HttpClient vengono eliminati quando xUnit elimina WebApplicationFactory
. Se gli oggetti creati dallo sviluppatore richiedono l'eliminazione, eliminarli nell'implementazione IClassFixture
. Per ulteriori informazioni, vedere Implementazione del metodo Dispose.
Esempio di test di integrazione
L'app di esempio è costituita da due app:
Applicazione | Directory del progetto | Descrizione |
---|---|---|
App Messaggi (SUT) | src/RazorPagesProject |
Consente a un utente di aggiungere, eliminare, eliminare tutti e analizzare i messaggi. |
Testare l'app | tests/RazorPagesProject.Tests |
Usato per il test di integrazione del 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 Messaggi (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.
†L'articolo di Entity Framework, Test con InMemory, spiega 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 utilizzando il Microsoft.AspNetCore.TestHost, che include il 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, un test di eliminazione, che richiede l'eliminazione di un record nel 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 test builder.ConfigureServices
viene eseguito dopo che è stato eseguito il 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
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 SPA, consigliamo 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:
- base di dati
- Sistema di gestione dei file
- 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 del SUT è configurato.
- Viene creato un client del server di test per inviare richieste all'app.
- Il Passaggio Disponi viene eseguito: l'applicazione 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 di test del progettobin
. - Imposta la root del content alla root del progetto di SUT, così che i file statici e le pagine/view siano trovati durante l'esecuzione dei 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 la versione xunit.runner.visualstudio
2.4.2 o successiva, il progetto di test deve fare riferimento al pacchetto Microsoft.NET.Test.Sdk
.
Entity Framework Core viene usato anche nei test. Guarda il file del progetto su GitHub.
Ambiente SUT
Se l'ambiente del SUT non è impostato, l'ambiente predefinito è 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 dell'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 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 fixture di classe () per indicare che contengono test e per fornire istanze di oggetti condivisi tra i test all'interno della classe.
La seguente classe di test, BasicTests
, utilizza WebApplicationFactory
per eseguire il bootstrap del SUT e fornire un HttpClient a un metodo di test, Get_EndpointsReturnSuccessAndCorrectContentType
. Il metodo verifica che il codice di stato della risposta sia compreso tra (200-299) e che l'intestazione Content-Type
sia text/html; charset=utf-8
per varie 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 contro 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 del controller e delle pagine Razor a un livello più basso, senza preoccuparsi di come vengono eseguiti 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:
Eredita da
WebApplicationFactory
e sovrascrivi ConfigureWebHost. 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(IDbContextOptionsConfiguration<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 attraverso il metodo
InitializeDbForTests
. Il metodo è descritto nella sezione Esempio di test di integrazione: Testare l'organizzazione dell'app.Il contesto del database di SUT viene registrato in
Program.cs
. Il callback dell'app di testbuilder.ConfigureServices
viene eseguito dopo che è stato eseguito il 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 fabbrica aggiunge quindi un nuovo
ApplicationDbContext
che utilizza un database in memoria per i test.Per connettersi a un database diverso, modificare il
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 il 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 utilizza
HttpClient
e i metodi helper 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 che viene effettuato automaticamente dal sistema di protezione dei dati antiforgery 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.
- Esegui la richiesta POST con il token di convalida e antifalsificazione cookie incluso.
I
-
GetDocumentAsync
: riceve HttpResponseMessage e restituisce un oggettoIHtmlDocument
.GetDocumentAsync
utilizza una fabbrica che prepara una risposta virtuale basata sull'originaleHttpResponseMessage
. Per altre informazioni, vedere la documentazione di AngleSharp. -
SendAsync
metodi di estensione perHttpClient
compongono un HttpRequestMessage e chiamano SendAsync(HttpRequestMessage) per inviare richieste al SUT. Overload perSendAsync
accettano il modulo HTML (IHtmlFormElement
) e gli elementi seguenti:- Pulsante Invia del modulo (
IHtmlElement
) - Raccolta di valori del modulo (
IEnumerable<KeyValuePair<string, string>>
) - Pulsante di invio (
IHtmlElement
) e valori del 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 nello scrivere codice per gestire direttamente il token di verifica delle richieste del sistema antiforgery e il componente antiforgery cookie. Per ulteriori informazioni, vedere AngleSharp vs Application Parts
per i controlli antifalsificazione 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.
Vedi Estendere il processo di avvio con filtri di avvio, che mostra come configurare il middleware usando IStartupFilter, utile quando un test richiede un servizio o un middleware personalizzato.
Personalizzare il client con WithWebHostBuilder
Quando è necessaria una configurazione aggiuntiva all'interno di un metodo di test, WithWebHostBuilder crea un nuovo WebApplicationFactory
con un IWebHostBuilder 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 WebApplicationFactoryClientOptions istanze, vedere la HttpClient
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 simulati
I servizi possono essere sovrascritti 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
- Ottieni_PaginaSicuraRestituitaPerUtenteAutenticato
L'esempio SUT include un servizio con ambito definito che restituisce una citazione. La citazione è incorporata in un campo nascosto nella pagina Indice quando questa viene richiesta.
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 a scopo è 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 della citazione fornito da TestQuoteService
, quindi l'asserzione viene superata.
<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.
Nella SUT, la pagina /SecurePage
utilizza una convenzione AuthorizePage per applicare un AuthorizeFilter alla pagina. Per ulteriori informazioni, vedere Razor convenzioni di autorizzazione delle pagine.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Get_SecurePageRedirectsAnUnauthenticatedUser
Nel test, un WebApplicationFactoryClientOptions è impostato per 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 valore dell'intestazione
Location
nelle intestazioni della risposta viene controllato per confermare che inizia conhttp://localhost/Identity/Account/Login
, anziché 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 delle opzioni del client.
Test di base per il middleware di autenticazione
Consulta 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(IDbContextOptionsConfiguration<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 costruttore deduce il percorso radice del contenuto dell'app cercando un elemento WebApplicationFactory
nell'assembly che contiene i test di integrazione, con una chiave uguale a quella dell'assembly WebApplicationFactoryContentRootAttribute. 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 delle ombre
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 la corretta impostazione di configurazione:
{
"shadowCopy": false
}
Eliminazione di oggetti
Dopo l'esecuzione dei test dell'implementazione IClassFixture
, TestServer e HttpClient vengono eliminati quando xUnit elimina WebApplicationFactory
. Se gli oggetti creati dallo sviluppatore richiedono l'eliminazione, eliminarli nell'implementazione IClassFixture
. Per ulteriori informazioni, vedere Implementazione di un metodo Dispose.
Esempio di test di integrazione
L'app di esempio è costituita da due app:
Applicazione | Directory del progetto | Descrizione |
---|---|---|
App Messaggi (SUT) | src/RazorPagesProject |
Consente a un utente di aggiungere, eliminare, eliminare tutti e analizzare i messaggi. |
Testare l'app | tests/RazorPagesProject.Tests |
Usato per il test di integrazione del 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 Messaggi (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.
†L'articolo di Entity Framework, Test con InMemory, spiega 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 utilizzando il Microsoft.AspNetCore.TestHost, che include il 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, un test di eliminazione, che richiede l'eliminazione di un record nel 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 test builder.ConfigureServices
viene eseguito dopo che è stato eseguito il 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 .