Integrationstests in ASP.NET Core
Von Jos van der Til, Martin Costello und Javier Calvarro Nelson.
Integrationstests stellen sicher, dass die Komponenten einer App auf einer Ebene, die die unterstützende Infrastruktur der App (wie die Datenbank, das Dateisystem und das Netzwerk) umfasst, ordnungsgemäß funktionieren. ASP.NET Core unterstützt Integrationstests mithilfe eines Komponententest-Frameworks mit einem Testwebhost und einem In-Memory-Testserver.
Dieser Artikel setzt Grundkenntnisse zu Komponententests voraus. Wenn Sie nicht mit Testkonzepten vertraut sind, lesen Sie den Artikel Komponententests in .NET Core und .NET Standard und zugehörige Inhalte.
Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)
Bei der Beispiel-App handelt es sich um eine Razor Pages-App, hierfür werden grundlegende Kenntnisse über Razor Pages vorausgesetzt. Wenn Sie nicht mit Razor Pages vertraut sind, lesen Sie die folgenden Artikel:
Zum Testen von Single-Page-Webanwendungen empfiehlt sich ein Tool wie Playwright für .NET, mit dem ein Browser automatisiert werden kann.
Einführung in Integrationstests
Integrationstests bewerten die Komponenten einer App auf breiterer Ebene als Komponententests. Komponententests werden verwendet, um isolierte Softwarekomponenten wie z. B. einzelne Klassenmethoden zu testen. Integrationstests bestätigen, dass zwei oder mehr App-Komponenten zusammenarbeiten, um ein erwartetes Ergebnis zu erzielen, ggf. auch unter Einbindung aller Komponenten, die für die vollständige Verarbeitung einer Anforderung erforderlich sind.
Diese umfassenderen Tests werden verwendet, um die Infrastruktur und das gesamte Framework der App zu testen, häufig einschließlich der folgenden Komponenten:
- Datenbank
- Dateisystem
- Netzwerk-Appliances
- Anforderung/Antwort-Pipeline
Komponententests verwenden anstelle von Infrastrukturkomponenten künstliche Komponenten, die als Fakes oder Pseudoobjekte bezeichnet werden.
Im Gegensatz zu Komponententests gilt für Integrationstests:
- Sie verwenden die tatsächlichen Komponenten, die von der App in der Produktionsumgebung verwendet werden.
- Sie erfordern mehr Code und Datenverarbeitung.
- Ihre Ausführung dauert länger.
Beschränken Sie daher die Verwendung von Integrationstests auf die wichtigsten Infrastrukturszenarios. Wenn ein Verhalten mithilfe eines Komponententests oder eines Integrationstests getestet werden kann, wählen Sie den Komponententest.
Bei der Besprechung von Integrationstests wird das getestete Projekt häufig als getestetes System oder kurz GS (englisch System Under Test, SUT) bezeichnet. In diesem Artikel wird zum Verweis auf die zu testende ASP.NET Core-Anwendung der Begriff „GS“ verwendet.
Schreiben Sie keine Integrationstests für jede Permutation des Daten- und Dateizugriffs bei Datenbanken und Dateisystemen. Unabhängig davon, wie viele Elemente in einer App mit Datenbanken und Dateisystemen interagieren, ist ein fokussierter Satz von Lese-, Schreib-, Update- und Lösch-Integrationstests üblicherweise in der Lage, die Datenbank- und Dateisystemkomponenten adäquat zu testen. Verwenden Sie Komponententests für Routinetests der Methodenlogik, die mit diesen Komponenten interagieren. Bei Komponententests beschleunigt die Verwendung von Fake- oder Pseudoergebnissen für eine Infrastruktur die Testausführung.
Integrationstests in ASP.NET Core
Integrationstests in ASP.NET Core erfordern Folgendes:
- Es wird ein Testprojekt verwendet, um die Tests einzugrenzen und auszuführen. Das Testprojekt enthält einen Verweis auf das GS.
- Das Testprojekt erstellt einen Testwebhost für das GS und verwendet einen Testserverclient, um Anforderungen und Antworten im Zusammenhang mit dem GS zu verarbeiten.
- Um die Tests auszuführen und die Testergebnisse zu melden, wird ein Test-Runner verwendet.
Integrationstests folgen einer Sequenz von Ereignissen, die die üblichen Testschritte Arrange, Act und Assert umfassen:
- Der Webhost des GS wird konfiguriert.
- Es wird ein Testserverclient erstellt, um Anforderungen an die App zu senden.
- Der Testschritt Arrange wird ausgeführt: Die Test-App bereitet eine Anforderung vor.
- Der Testschritt Act wird ausgeführt: Der Client sendet die Anforderung und empfängt die Antwort.
- Der Testschritt Assert wird ausgeführt: Die tatsächliche Antwort wird je nach der erwarteten Antwort als Pass oder Fail bewertet.
- Der Prozess wird so lange fortgesetzt, bis alle Tests ausgeführt wurden.
- Die Testergebnisse werden gemeldet.
Üblicherweise ist der Testwebhost anders konfiguriert als der normale Webhost der App für die Testläufe. Beispielsweise könnten für die Tests eine andere Datenbank oder andere App-Einstellungen verwendet werden.
Infrastrukturkomponenten wie der Testwebhost und der In-Memory-Testserver (TestServer) werden durch das Paket Microsoft.AspNetCore.Mvc.Testing bereitgestellt oder verwaltet. Durch die Verwendung dieses Pakets werden die Testerstellung und -ausführung optimiert.
Das Microsoft.AspNetCore.Mvc.Testing
-Paket verarbeitet die folgenden Aufgaben:
- Es kopiert die Datei für Abhängigkeiten (
.deps
) aus dem GS in dasbin
Verzeichnis des Testprojekts. - Es legt das Inhaltsstammelement auf das Projektstammelement des GS fest, damit statische Dateien und Seiten/Ansichten bei der Ausführung der Tests gefunden werden.
- Es stellt die Klasse WebApplicationFactory zur Optimierung des Bootstrappings des GS mit
TestServer
bereit.
In der Dokumentation der Komponententests wird beschrieben, wie Sie ein Testprojekt und einen Test-Runner einrichten. Ferner finden Sie dort ausführliche Anweisungen zum Ausführen von Tests sowie Empfehlungen zum Benennen von Tests und Testklassen.
Unterteilen Sie Komponententests und Integrationstests in verschiedene Projekte. Trennen der Tests:
- Damit wird sichergestellt, dass Komponenten für Infrastrukturtests nicht versehentlich in die Komponententests eingeschlossen werden.
- So können Sie steuern, welche Testsätze ausgeführt werden.
Es gibt praktisch keinen Unterschied zwischen der Konfiguration für Tests von Razor Pages-Apps und MVC-Apps. Der einzige Unterschied besteht darin, wie die Tests benannt werden. In einer Razor Pages-App werden Tests von Seitenendpunkten normalerweise nach der Seitenmodellklasse benannt (z. B. IndexPageTests
für das Testen der Komponentenintegration für die Indexseite). In einer MVC-App werden Tests in der Regel nach Controllerklassen organisiert und nach den von ihnen getesteten Controllern benannt (z. B. HomeControllerTests
für das Testen der Komponentenintegration für den Home-Controller).
Voraussetzungen für Test-Apps
Für das Testprojekt muss Folgendes erfüllt sein:
- Verweisen Sie auf das
Microsoft.AspNetCore.Mvc.Testing
-Paket. - Geben Sie das Web SDK in der Projektdatei an (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Diese Voraussetzungen können Sie in der Beispiel-App sehen. Sehen Sie sich die Datei tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
an. Die Beispiel-App verwendet das xUnit-Testframework und die AngleSharp-Parserbibliothek, sodass die Beispiel-APP auch auf Folgendes verweist:
In Apps, die Version 2.4.2 oder höher von xunit.runner.visualstudio
verwenden, muss das Testprojekt auf das Paket Microsoft.NET.Test.Sdk
verweisen.
Entity Framework Core wird ebenfalls in den Tests verwendet. Weitere Informationen finden Sie auch in der Projektdatei auf GitHub.
GS-Umgebung
Wenn die Umgebung des GS nicht festgelegt ist, wird standardmäßig die Entwicklungsumgebung verwendet.
Grundlegende Tests mit der Standard-WebApplicationFactory
Machen Sie die implizit definierte Program
-Klasse auf eine der folgenden Arten für das Testprojekt verfügbar:
Machen Sie interne Typen aus der Web-App für das Testprojekt verfügbar. Dies ist in der Projektdatei (
.csproj
) für das GS möglich:<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
Legen Sie die
Program
-Klasse mithilfe einer partiellen Klassendeklaration als öffentlich fest:var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
Die Beispiel-App verfolgt einen Ansatz mit der partiellen
Program
-Klasse.
WebApplicationFactory<TEntryPoint> wird verwendet, um eine TestServer-Klasse für die Integrationstests zu erstellen. TEntryPoint
ist die Einstiegspunktklasse des GS, in der Regel Program.cs
.
Testklassen implementieren eine Klassenfixture-Schnittstelle (IClassFixture
), um anzugeben, dass die Klasse Tests enthält und um gemeinsame Objektinstanzen in den Tests in der Klasse bereitzustellen.
Die folgende Testklasse (BasicTests
) verwendet die WebApplicationFactory
für den Bootstrap des GS und um eine HttpClient-Klasse für die Testmethode Get_EndpointsReturnSuccessAndCorrectContentType
bereitzustellen. Die Methode prüft, ob die Antwort einen erfolgreichen Statuscode (200–299) enthält und der Content-Type
-Header für mehrere App-Seiten text/html; charset=utf-8
lautet.
CreateClient() erstellt eine Instanz von HttpClient
, die automatisch Umleitungen folgt und Cookies verarbeitet.
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());
}
}
Standardmäßig werden nicht erforderliche Cookies nicht über Anforderungen hinweg beibehalten, wenn die Einwilligungsrichtlinie zur Datenschutz-Grundverordnung (DSGVO) aktiviert ist. Um nicht erforderliche Cookies beizubehalten, wie z. B. diejenigen, die vom TempData-Anbieter verwendet werden, markieren Sie die Cookies in den Tests als unverzichtbar. Anweisungen zum Markieren eines Cookies als erforderlich finden Sie unter Erforderliche Cookies.
AngleSharp oder Application Parts
für Antifälschungsüberprüfungen
In diesem Artikel wird der AngleSharp-Parser für die Antifälschungsüberprüfungen verwendet, indem Seiten geladen werden und der HTML-Code analysiert wird. Wenn Sie die Endpunkte des Controllers und die Besuche von Razor-Seiten auf niedrigerer Ebene testen möchten, ohne dabei das Rendern im Browser beachten zu müssen, sollten Sie die Verwendung von Application Parts
in Erwägung ziehen. Beim Ansatz mit Anwendungsteilen wird ein Controller oder eine Razor-Seite in die App injiziert, mit dem oder der JSON-Anforderungen erstellt werden können, um die erforderlichen Werte abzurufen. Weitere Informationen finden Sie im Blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts (Integrationstests bei ASP.NET Core-Ressourcen mit Fälschungsschutz durch Anwendungsteile) und dem zugehörigen GitHub-Repository von Martin Costello.
Anpassen von WebApplicationFactory
Die Webhostkonfiguration kann unabhängig von den Testklassen durch Erben von der WebApplicationFactory<TEntryPoint> erstellt werden, um eine oder mehrere benutzerdefinierte Factorys zu erstellen:
Führen Sie eine Vererbung von
WebApplicationFactory
durch, und überschreiben Sie ConfigureWebHost. Der IWebHostBuilder ermöglicht die Konfiguration der Serversammlung mitIWebHostBuilder.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"); } }
Das Datenbankseeding in der Beispiel-App wird mithilfe der
InitializeDbForTests
-Methode durchgeführt. Die Methode wird im Abschnitt Beispiel für Integrationstests: Organisation der Test-App beschrieben.Der Datenbankkontext des GS wird in
Program.cs
registriert. Derbuilder.ConfigureServices
-Rückruf der Test-App wird ausgeführt, nachdem derProgram.cs
-Code der App ausgeführt wurde. Um für die Tests eine andere Datenbank als die Datenbank der App zu verwenden, muss der Datenbankkontext der App inbuilder.ConfigureServices
ersetzt werden.Die Beispiel-App findet den Dienstdeskriptor für den Datenbankkontext und verwendet den Deskriptor, um die Dienstregistrierung zu entfernen. Anschließend fügt die Factory einen neuen
ApplicationDbContext
hinzu, der eine Datenbank im Arbeitsspeicher für die Tests verwendet.Wenn Sie eine Verbindung mit einer anderen Datenbank herstellen möchten, ändern Sie die
DbConnection
. So verwenden Sie eine SQL Server-Testdatenbank:- Verweisen Sie in der Projektdatei auf das NuGet-Paket
Microsoft.EntityFrameworkCore.SqlServer
. - Rufen Sie
UseInMemoryDatabase
auf.
- Verweisen Sie in der Projektdatei auf das NuGet-Paket
Verwenden Sie die benutzerdefinierte
CustomWebApplicationFactory
in Testklassen. Das folgende Beispiel verwendet die Factory in derIndexPageTests
-Klasse: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 }); }
Der Client der Beispiel-App wird so konfiguriert, dass der
HttpClient
keinen Umleitungen folgt. Wie weiter unten im Abschnitt Pseudoauthentifizierung erläutert wird, können Tests so das Ergebnis der ersten Reaktion der App überprüfen. In vielen dieser Tests mit einemLocation
-Header ist die erste Antwort eine Umleitung.Ein typischer Test verwendet die
HttpClient
- und die Hilfsprogrammmethoden, um die Anforderung und die Antwort zu verarbeiten:[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); }
Jede POST-Anforderung an das GS muss die Fälschungsschutzprüfung bestehen, die automatisch vom Daten- und Fälschungsschutzsystem der App durchgeführt wird. Als Vorbereitung auf die POST-Anforderung eines Tests muss die Test-App folgende Schritte ausführen:
- Senden einer Anforderung für die Seite.
- Analysieren des Fälschungsschutzcookies und des Anforderungsvalidierungstokens von der Antwort.
- Senden der POST-Anforderung mit vorhandenem Fälschungsschutzcookie und Anforderungsvalidierungstoken.
Die SendAsync
-Hilfsprogrammerweiterungsmethoden (Helpers/HttpClientExtensions.cs
) und die GetDocumentAsync
-Hilfsprogrammmethode (Helpers/HtmlHelpers.cs
) in der Beispiel-App verwenden den AngleSharp-Parser, um die Fälschungsschutzprüfungen mit den folgenden Methoden durchzuführen:
GetDocumentAsync
: empfängt HttpResponseMessage und gibtIHtmlDocument
zurückGetDocumentAsync
verwendet eine Factory, die eine virtuelle Antwort basierend auf der ursprünglichenHttpResponseMessage
vorbereitet. Weitere Informationen finden Sie in der AngleSharp-Dokumentation.SendAsync
-Erweiterungsmethoden fürHttpClient
erstellen eine HttpRequestMessage-Klasse und rufen SendAsync(HttpRequestMessage) auf, um Anforderungen an das GS zu übermitteln. Überladungen fürSendAsync
akzeptieren das HTML-Formular (IHtmlFormElement
) und Folgendes:- Schaltfläche „Senden“ des Formulars (
IHtmlElement
) - Formularwerteauflistung (
IEnumerable<KeyValuePair<string, string>>
) - Schaltfläche „Senden“ (
IHtmlElement
) und Formularwerte (IEnumerable<KeyValuePair<string, string>>
)
- Schaltfläche „Senden“ des Formulars (
AngleSharp ist eine Drittanbieter-Analysebibliothek, die in diesem Artikel und in der Beispiel-App zu Demonstrationszwecken verwendet wird. AngleSharp wird für Integrationstests von ASP.NET Core-Apps weder unterstützt noch benötigt. Andere Parser können verwendet werden, beispielsweise Html Agility Pack (HAP). Ein anderer Ansatz besteht darin, Code zu schreiben, der das Anforderungsüberprüfungstoken und das Fälschungsschutzcookie des Fälschungsschutzsystems direkt verarbeitet. Weitere Informationen finden Sie unter AngleSharp oder Application Parts
für Antifälschungsüberprüfungen in diesem Artikel.
Der In-Memory-Datenbankanbieter von EF Core kann für begrenzte und grundlegende Tests verwendet werden, der SQLite-Anbieter ist jedoch die empfohlene Option für Tests im Arbeitsspeicher.
Weitere Informationen finden Sie unter Erweitern des Startvorgangs mit Startfiltern. Darin wird gezeigt, wie Middleware mithilfe von IStartupFilter konfiguriert wird. Das kann nützlich sein, wenn für einen Test ein benutzerdefinierter Dienst oder eine benutzerdefinierte Middleware erforderlich ist.
Anpassen des Clients mit WithWebHostBuilder
Wenn eine zusätzliche Konfiguration innerhalb einer Testmethode erforderlich ist, erstellt WithWebHostBuilder eine neue WebApplicationFactory
-Klasse mit einer IWebHostBuilder-Schnittstelle, die weiter konfiguriert wird.
Der Beispielcode ruft WithWebHostBuilder
auf, um konfigurierte Dienste durch Test-Stubs zu ersetzen. Weitere Informationen und Beispielnutzung finden Sie in dem Artikel Injizieren von Pseudodiensten.
Die Post_DeleteMessageHandler_ReturnsRedirectToRoot
-Testmethode der Beispiel-App zeigt die Verwendung von WithWebHostBuilder
. Bei diesem Test wird eine Datensatzlöschung in der Datenbank durch Auslösen einer Formularübermittlung im GS durchführt.
Da ein anderer Test in der IndexPageTests
-Klasse einen Vorgang durchführt, der alle Datensätze in der Datenbank löscht und der möglicherweise vor der Post_DeleteMessageHandler_ReturnsRedirectToRoot
-Methode ausgeführt wird, wird in dieser Testmethode ein erneutes Seeding der Datenbank durchgeführt, um sicherzustellen, dass ein Datensatz vorhanden ist, den das GS löschen kann. Die Auswahl der ersten Löschschaltfläche des messages
-Formulars im GS wird in der Anforderung an das GS simuliert:
[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);
}
Clientoptionen
Auf der WebApplicationFactoryClientOptions-Seite finden Sie die Standardeinstellungen und verfügbaren Optionen für das Erstellen von HttpClient
-Instanzen.
Erstellen Sie die WebApplicationFactoryClientOptions
-Klasse, und übergeben Sie sie an die CreateClient()-Methode:
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
});
}
HINWEIS: Um HTTPS-Umleitungswarnungen in Protokollen bei Verwendung von HTTPS-Umleitungsmiddleware zu vermeiden, legen Sie die Einstellung BaseAddress = new Uri("https://localhost")
fest.
Fügen Sie Pseudodienste ein
Dienste können in einem Test überschrieben werden, indem ConfigureTestServices im Host-Generator aufgerufen wird. Um die Außerkraftsetzungsdienste auf den Test selbst zu beschränken, wird die WithWebHostBuilder-Methode zum Abrufen eines Host-Generators verwendet. Dies kann in den folgenden Tests zu sehen sein:
- Get_QuoteService_ProvidesQuoteInPage
- Get_GithubProfilePageCanGetAGithubUser
- Get_SecurePageIsReturnedForAnAuthenticatedUser
Das Beispiel-GS enthält einen bereichsbezogenen Dienst, der ein Zitat zurückgibt. Wenn die Indexseite angefordert wird, wird das Zitat in ein ausgeblendetes Feld auf der Indexseite eingebettet.
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">
Das folgende Markup wird generiert, wenn die GS-App ausgeführt wird:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
Um den Dienst und die Zitateinfügung in einem Integrationstest zu testen, wird vom Test ein Pseudodienst in das GS eingefügt. Der Pseudodienst ersetzt QuoteService
der App durch einen Dienst namens TestQuoteService
, der von der Test-App bereitgestellt wird:
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
wird aufgerufen, und der bereichsbezogene Dienst wird registriert:
[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);
}
Das während der Ausführung des Tests erstellte Markup gibt das von TestQuoteService
bereitgestellte Zitat wieder, somit ist die Assertion erfolgreich:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Pseudo-Authentifizierung
Tests in der AuthTests
-Klasse prüfen, ob ein sicherer Endpunkt:
- Nicht authentifizierte Benutzer*innen auf die Anmeldeseite der App umleitet
- Den Inhalt für einen authentifizierten Benutzer zurückgibt.
Im GS verwendet die Seite /SecurePage
die Konvention AuthorizePage, um AuthorizeFilter auf die Seite anzuwenden. Weitere Informationen finden Sie unter Razor Pages-Autorisierungskonventionen.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Im Test Get_SecurePageRedirectsAnUnauthenticatedUser
wird WebApplicationFactoryClientOptions so eingestellt, dass Umleitungen nicht zulässig sind. Hierfür wird AllowAutoRedirect auf false
festgelegt:
[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);
}
Indem dem Client untersagt wird, die Umleitung zu befolgen, können folgende Prüfungen durchgeführt werden:
- Der vom GS zurückgegebene Statuscode kann mit dem erwarteten Ergebnis für HttpStatusCode.Redirect verglichen werden, anstatt mit dem endgültigen Statuscode nach der Umleitung zur Anmeldeseite (HttpStatusCode.OK).
- Der Wert für den
Location
-Header in den Antwortheadern wird geprüft, um zu bestätigen, dass er mithttp://localhost/Identity/Account/Login
beginnt. Es wird nicht die abschließende Antwort der Anmeldeseite verwendet, bei der derLocation
-Header nicht vorhanden wäre.
Die Test-App kann AuthenticationHandler<TOptions> in ConfigureTestServices simulieren, um Aspekte der Authentifizierung und Autorisierung zu testen. Ein minimales Szenario gibt AuthenticateResult.Success zurück:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder)
: base(options, logger, encoder)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
wird aufgerufen, um einen Benutzer zu authentifizieren, wenn das Authentifizierungsschema auf TestScheme
festgelegt wird, in dem AddAuthentication
für ConfigureTestServices
registriert ist. Es ist wichtig, dass das TestScheme
-Schema mit dem Schema übereinstimmt, das Ihre App erwartet. Andernfalls funktioniert die Authentifizierung nicht.
[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);
}
Weitere Informationen zu WebApplicationFactoryClientOptions
finden Sie im Abschnitt Clientoptionen.
Grundlegende Tests für Middleware zur Authentifizierung
In diesem GitHub-Repository finden Sie grundlegende Tests für Middleware zur Authentifizierung. Es enthält einen Testserver, der für das Testszenario spezifisch ist.
Festlegen der Umgebung
Legen Sie die Umgebung in der benutzerdefinierten Anwendungsfactory fest:
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");
}
}
Ableitung des Inhaltsstammpfads der App durch die Testinfrastruktur
Der Konstruktor WebApplicationFactory
leitet den Inhaltsstammpfad der App ab, indem er in der Assembly, die die Integrationstests enthält, nach einer WebApplicationFactoryContentRootAttribute-Klasse mit einem Schlüssel sucht, der der TEntryPoint
-Assembly System.Reflection.Assembly.FullName
entspricht. Wenn kein Attribut mit dem richtigen Schlüssel gefunden wird, greift WebApplicationFactory
auf die Suche nach einer Projektmappendatei ( .sln) zurück und fügt den TEntryPoint
-Assemblynamen an das Projektmappenverzeichnis an. Das Stammverzeichnis der App (der Inhaltsstammpfad) wird verwendet, um Sichten und Inhaltsdateien zu ermitteln.
Deaktivieren der Erstellung von Schattenkopien
Das Erstellen von Schattenkopien bewirkt, dass die Tests in einem anderen Verzeichnis als dem Ausgabeverzeichnis ausgeführt werden. Wenn Ihre Tests auf dem Laden von Dateien relativ zu Assembly.Location
basieren und Probleme auftreten, müssen Sie möglicherweise das Schattenkopiervorgang deaktivieren.
Um das Schattenkopieren bei Verwendung von xUnit zu deaktivieren, erstellen Sie eine xunit.runner.json
-Datei in Ihrem Testprojektverzeichnis mit der richtigen Konfigurationseinstellung:
{
"shadowCopy": false
}
Verwerfen von Objekten
Nachdem die Tests der IClassFixture
-Implementierung abgeschlossen sind, werden TestServer und HttpClient verworfen, wenn xUnit WebApplicationFactory
verwirft. Wenn vom Entwickler instanziierte Objekte verworfen werden müssen, müssen Sie dies in der IClassFixture
-Implementierung tun. Weitere Informationen finden Sie unter Implementieren einer Dispose-Methode.
Beispiel für Integrationstests
Die Beispiel-App besteht aus zwei Apps:
App | Projektverzeichnis | Beschreibung |
---|---|---|
Nachrichten-App (das GS) | src/RazorPagesProject |
Ermöglicht einem Benutzer, Nachrichten hinzuzufügen, eine oder alle Nachrichten zu löschen und Nachrichten zu analysieren. |
Testen der App | tests/RazorPagesProject.Tests |
Wird für den Integrationstest des GS verwendet. |
Die Tests können mit den integrierten Testfunktionen einer IDE, wie z. B. Visual Studio ausgeführt werden. Wenn Sie Visual Studio Code oder die Befehlszeile verwenden, führen Sie den folgenden Befehl über eine Eingabeaufforderung im Verzeichnis tests/RazorPagesProject.Tests
aus:
dotnet test
Organisation der Nachrichten-App (GS)
Beim GS handelt es sich um ein Razor Pages-Nachrichtensystem mit folgenden Merkmalen:
- Die Indexseite der App (
Pages/Index.cshtml
undPages/Index.cshtml.cs
) stellt eine Benutzeroberfläche und Seitenmodellmethoden bereit, mit denen Sie das Hinzufügen, Löschen und Analysieren von Nachrichten (durchschnittliche Anzahl von Wörtern pro Nachricht) steuern können. - Eine Nachricht wird von der
Message
-Klasse (Data/Message.cs
) mit zwei Eigenschaften beschrieben:Id
(Schlüssel) undText
(Nachricht). DieText
-Eigenschaft ist erforderlich und auf 200 Zeichen beschränkt. - Nachrichten werden mithilfe der In-Memory-Datenbank von Entity Framework† gespeichert.
- Die App enthält eine Datenzugriffsebene (DAL) in ihrer Datenbankkontextklasse
AppDbContext
(Data/AppDbContext.cs
). - Wenn die Datenbank beim Starten der App leer ist, wird der Nachrichtenspeicher mit drei Nachrichten initialisiert.
- Die App enthält eine
/SecurePage
, auf die nur ein authentifizierter Benutzer zugreifen kann.
† Im EF-Thema Testen mit InMemory wird die Verwendung einer Datenbank im Arbeitsspeicher für Tests mit MSTest erläutert. In diesem Thema wird das Testframework xUnit verwendet. Testkonzepte und Testimplementierungen in verschiedenen Testframeworks sind ähnlich, jedoch nicht identisch.
Obwohl die App nicht das Repositorymuster verwendet und kein effektives Beispiel für das Arbeitseinheitsmuster ist, unterstützt Razor Pages diese Entwicklungsmuster. Weitere Informationen finden Sie unter Entwerfen der Persistenzebene der Infrastruktur und Testcontrollerlogik (im Beispiel wird das Repositorymuster implementiert).
Organisation der Test-App
Die Test-App ist eine Konsolen-App im Verzeichnis tests/RazorPagesProject.Tests
.
Test-App-Verzeichnis | Beschreibung |
---|---|
AuthTests |
Enthält Testmethoden für Folgendes:
|
BasicTests |
Enthält eine Testmethode für Routing und Inhaltstyp. |
IntegrationTests |
Enthält die Integrationstests für die Indexseite unter Verwendung der benutzerdefinierten WebApplicationFactory -Klasse. |
Helpers/Utilities |
|
Das Testframework ist xUnit. Integrationstests werden mit der Klasse Microsoft.AspNetCore.TestHost durchgeführt, die TestServer enthält. Da das Paket Microsoft.AspNetCore.Mvc.Testing
zum Konfigurieren des Testhosts und des Testservers verwendet wird, benötigen die Pakete TestHost
und TestServer
keine direkten Paketverweise in der Projektdatei der Test-App bzw. keine Entwicklerkonfiguration in der Test-App.
Für Integrationstests muss die Datenbank in der Regel vor der Testausführung ein kleines Dataset enthalten. Beispielsweise wird bei einem Löschtest ein Löschvorgang eines Datensatzes der Datenbank abgerufen, weshalb die Datenbank mindestens einen Datensatz aufweisen muss, damit die Löschanforderung erfolgreich ausgeführt wird.
Die Beispiel-App führt ein Seeding der Datenbank mit drei Nachrichten in Utilities.cs
durch, die von Tests bei der Ausführung verwendet werden können:
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." }
};
}
Der Datenbankkontext des GS wird in Program.cs
registriert. Der builder.ConfigureServices
-Rückruf der Test-App wird ausgeführt, nachdem der Program.cs
-Code der App ausgeführt wurde. Um eine andere Datenbank für die Tests zu verwenden, muss der Datenbankkontext der App in builder.ConfigureServices
ersetzt werden. Weitere Informationen finden Sie im Abschnitt Anpassen von WebApplicationFactory.
Zusätzliche Ressourcen
In diesem Thema werden Grundkenntnisse über Komponententests vorausgesetzt. Wenn Sie nicht mit Testkonzepten vertraut sind, lesen Sie das Thema Komponententests in .NET Core und .NET Standard und zugehörige Inhalte.
Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)
Bei der Beispiel-App handelt es sich um eine Razor Pages-App, hierfür werden grundlegende Kenntnisse über Razor Pages vorausgesetzt. Wenn Sie nicht mit Razor Pages vertraut sind, lesen Sie die folgenden Themen:
Hinweis
Zum Testen von Single-Page-Webanwendungen empfiehlt sich ein Tool wie Playwright für .NET, mit dem ein Browser automatisiert werden kann.
Einführung in Integrationstests
Integrationstests bewerten die Komponenten einer App auf breiterer Ebene als Komponententests. Komponententests werden verwendet, um isolierte Softwarekomponenten wie z. B. einzelne Klassenmethoden zu testen. Integrationstests bestätigen, dass zwei oder mehr App-Komponenten zusammenarbeiten, um ein erwartetes Ergebnis zu erzielen, ggf. auch unter Einbindung aller Komponenten, die für die vollständige Verarbeitung einer Anforderung erforderlich sind.
Diese umfassenderen Tests werden verwendet, um die Infrastruktur und das gesamte Framework der App zu testen, häufig einschließlich der folgenden Komponenten:
- Datenbank
- Dateisystem
- Netzwerk-Appliances
- Anforderung/Antwort-Pipeline
Komponententests verwenden anstelle von Infrastrukturkomponenten künstliche Komponenten, die als Fakes oder Pseudoobjekte bezeichnet werden.
Im Gegensatz zu Komponententests gilt für Integrationstests:
- Sie verwenden die tatsächlichen Komponenten, die von der App in der Produktionsumgebung verwendet werden.
- Sie erfordern mehr Code und Datenverarbeitung.
- Ihre Ausführung dauert länger.
Beschränken Sie daher die Verwendung von Integrationstests auf die wichtigsten Infrastrukturszenarios. Wenn ein Verhalten mithilfe eines Komponententests oder eines Integrationstests getestet werden kann, wählen Sie den Komponententest.
Bei der Besprechung von Integrationstests wird das getestete Projekt häufig als getestetes System oder kurz GS (englisch System Under Test, SUT) bezeichnet. In diesem Artikel wird zum Verweis auf die zu testende ASP.NET Core-Anwendung der Begriff „GS“ verwendet.
Schreiben Sie keine Integrationstests für jede Permutation des Daten- und Dateizugriffs bei Datenbanken und Dateisystemen. Unabhängig davon, wie viele Elemente in einer App mit Datenbanken und Dateisystemen interagieren, ist ein fokussierter Satz von Lese-, Schreib-, Update- und Lösch-Integrationstests üblicherweise in der Lage, die Datenbank- und Dateisystemkomponenten adäquat zu testen. Verwenden Sie Komponententests für Routinetests der Methodenlogik, die mit diesen Komponenten interagieren. Bei Komponententests beschleunigt die Verwendung von Fake- oder Pseudoergebnissen für eine Infrastruktur die Testausführung.
Integrationstests in ASP.NET Core
Integrationstests in ASP.NET Core erfordern Folgendes:
- Es wird ein Testprojekt verwendet, um die Tests einzugrenzen und auszuführen. Das Testprojekt enthält einen Verweis auf das GS.
- Das Testprojekt erstellt einen Testwebhost für das GS und verwendet einen Testserverclient, um Anforderungen und Antworten im Zusammenhang mit dem GS zu verarbeiten.
- Um die Tests auszuführen und die Testergebnisse zu melden, wird ein Test-Runner verwendet.
Integrationstests folgen einer Sequenz von Ereignissen, die die üblichen Testschritte Arrange, Act und Assert umfassen:
- Der Webhost des GS wird konfiguriert.
- Es wird ein Testserverclient erstellt, um Anforderungen an die App zu senden.
- Der Testschritt Arrange wird ausgeführt: Die Test-App bereitet eine Anforderung vor.
- Der Testschritt Act wird ausgeführt: Der Client sendet die Anforderung und empfängt die Antwort.
- Der Testschritt Assert wird ausgeführt: Die tatsächliche Antwort wird je nach der erwarteten Antwort als Pass oder Fail bewertet.
- Der Prozess wird so lange fortgesetzt, bis alle Tests ausgeführt wurden.
- Die Testergebnisse werden gemeldet.
Üblicherweise ist der Testwebhost anders konfiguriert als der normale Webhost der App für die Testläufe. Beispielsweise könnten für die Tests eine andere Datenbank oder andere App-Einstellungen verwendet werden.
Infrastrukturkomponenten wie der Testwebhost und der In-Memory-Testserver (TestServer) werden durch das Paket Microsoft.AspNetCore.Mvc.Testing bereitgestellt oder verwaltet. Durch die Verwendung dieses Pakets werden die Testerstellung und -ausführung optimiert.
Das Microsoft.AspNetCore.Mvc.Testing
-Paket verarbeitet die folgenden Aufgaben:
- Es kopiert die Datei für Abhängigkeiten (
.deps
) aus dem GS in dasbin
Verzeichnis des Testprojekts. - Es legt das Inhaltsstammelement auf das Projektstammelement des GS fest, damit statische Dateien und Seiten/Ansichten bei der Ausführung der Tests gefunden werden.
- Es stellt die Klasse WebApplicationFactory zur Optimierung des Bootstrappings des GS mit
TestServer
bereit.
In der Dokumentation der Komponententests wird beschrieben, wie Sie ein Testprojekt und einen Test-Runner einrichten. Ferner finden Sie dort ausführliche Anweisungen zum Ausführen von Tests sowie Empfehlungen zum Benennen von Tests und Testklassen.
Unterteilen Sie Komponententests und Integrationstests in verschiedene Projekte. Trennen der Tests:
- Damit wird sichergestellt, dass Komponenten für Infrastrukturtests nicht versehentlich in die Komponententests eingeschlossen werden.
- So können Sie steuern, welche Testsätze ausgeführt werden.
Es gibt praktisch keinen Unterschied zwischen der Konfiguration für Tests von Razor Pages-Apps und MVC-Apps. Der einzige Unterschied besteht darin, wie die Tests benannt werden. In einer Razor Pages-App werden Tests von Seitenendpunkten normalerweise nach der Seitenmodellklasse benannt (z. B. IndexPageTests
für das Testen der Komponentenintegration für die Indexseite). In einer MVC-App werden Tests in der Regel nach Controllerklassen organisiert und nach den von ihnen getesteten Controllern benannt (z. B. HomeControllerTests
für das Testen der Komponentenintegration für den Home-Controller).
Voraussetzungen für Test-Apps
Für das Testprojekt muss Folgendes erfüllt sein:
- Verweisen Sie auf das
Microsoft.AspNetCore.Mvc.Testing
-Paket. - Geben Sie das Web SDK in der Projektdatei an (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Diese Voraussetzungen können Sie in der Beispiel-App sehen. Sehen Sie sich die Datei tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
an. Die Beispiel-App verwendet das xUnit-Testframework und die AngleSharp-Parserbibliothek, sodass die Beispiel-APP auch auf Folgendes verweist:
In Apps, die Version 2.4.2 oder höher von xunit.runner.visualstudio
verwenden, muss das Testprojekt auf das Paket Microsoft.NET.Test.Sdk
verweisen.
Entity Framework Core wird ebenfalls in den Tests verwendet. Die App verweist auf:
Microsoft.AspNetCore.Diagnostics.EntityFrameworkCore
Microsoft.AspNetCore.Identity.EntityFrameworkCore
Microsoft.EntityFrameworkCore
Microsoft.EntityFrameworkCore.InMemory
Microsoft.EntityFrameworkCore.Tools
GS-Umgebung
Wenn die Umgebung des GS nicht festgelegt ist, wird standardmäßig die Entwicklungsumgebung verwendet.
Grundlegende Tests mit der Standard-WebApplicationFactory
WebApplicationFactory<TEntryPoint> wird verwendet, um eine TestServer-Klasse für die Integrationstests zu erstellen. TEntryPoint
ist die Einstiegspunktklasse des GS, in der Regel die Startup
-Klasse.
Testklassen implementieren eine Klassenfixture-Schnittstelle (IClassFixture
), um anzugeben, dass die Klasse Tests enthält und um gemeinsame Objektinstanzen in den Tests in der Klasse bereitzustellen.
Die folgende Testklasse (BasicTests
) verwendet die WebApplicationFactory
für den Bootstrap des GS und um eine HttpClient-Klasse für die Testmethode Get_EndpointsReturnSuccessAndCorrectContentType
bereitzustellen. Die Methode prüft, ob der Antwortstatuscode erfolgreich ist (Statuscodes im Bereich 200-299) und der Content-Type
-Header für mehrere App-Seiten text/html; charset=utf-8
lautet.
CreateClient() erstellt eine Instanz von HttpClient
, die automatisch Umleitungen folgt und Cookies verarbeitet.
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());
}
}
Standardmäßig werden nicht erforderliche Cookies nicht über Anforderungen hinweg beibehalten, wenn die DSGVO-Zustimmungsrichtlinie aktiviert ist. Um nicht erforderliche Cookies beizubehalten, wie z. B. diejenigen, die vom TempData-Anbieter verwendet werden, markieren Sie die Cookies in den Tests als unverzichtbar. Anweisungen zum Markieren eines Cookies als erforderlich finden Sie unter Erforderliche Cookies.
Anpassen von WebApplicationFactory
Die Webhostkonfiguration kann unabhängig von den Testklassen durch Erben von der WebApplicationFactory
erstellt werden, um eine oder mehrere benutzerdefinierte Factorys zu erstellen:
Führen Sie eine Vererbung von
WebApplicationFactory
durch, und überschreiben Sie ConfigureWebHost. IWebHostBuilder ermöglicht die Konfiguration der Serversammlung mit 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); } } }); } }
Das Datenbankseeding in der Beispiel-App wird mithilfe der
InitializeDbForTests
-Methode durchgeführt. Die Methode wird im Abschnitt Beispiel für Integrationstests: Organisation der Test-App beschrieben.Der Datenbankkontext des GS wird in dessen
Startup.ConfigureServices
-Methode registriert. Derbuilder.ConfigureServices
-Rückruf der Test-App wird ausgeführt, nachdem derStartup.ConfigureServices
-Code der App ausgeführt wurde. Die Ausführungsreihenfolge ist im Release von ASP.NET Core 3.0 eine Breaking Change für den generischen Host. Um für die Tests eine andere Datenbank als die Datenbank der App zu verwenden, muss der Datenbankkontext der App inbuilder.ConfigureServices
ersetzt werden.Für GS, die weiterhin den Webhost verwenden, wird der
builder.ConfigureServices
-Rückruf der Test-App ausgeführt, bevor derStartup.ConfigureServices
-Code des GS ausgeführt wird. Derbuilder.ConfigureTestServices
-Rückruf der Test-App wird danach ausgeführt.Die Beispiel-App findet den Dienstdeskriptor für den Datenbankkontext und verwendet den Deskriptor, um die Dienstregistrierung zu entfernen. Als Nächstes fügt die Factory einen neuen
ApplicationDbContext
hinzu, der eine In-Memory-Datenbank für die Tests verwendet.Um eine Verbindung mit einer anderen Datenbank als der In-Memory-Datenbank herzustellen, ändern Sie den
UseInMemoryDatabase
-Aufruf, um den Kontext mit einer anderen Datenbank zu verbinden. So verwenden Sie eine SQL Server-Testdatenbank:- Verweisen Sie in der Projektdatei auf das NuGet-Paket
Microsoft.EntityFrameworkCore.SqlServer
. - Rufen Sie
UseSqlServer
mit einer Verbindungszeichenfolge zu der Datenbank auf.
services.AddDbContext<ApplicationDbContext>((options, context) => { context.UseSqlServer( Configuration.GetConnectionString("TestingDbConnectionString")); });
- Verweisen Sie in der Projektdatei auf das NuGet-Paket
Verwenden Sie die benutzerdefinierte
CustomWebApplicationFactory
in Testklassen. Das folgende Beispiel verwendet die Factory in derIndexPageTests
-Klasse: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 }); }
Der Client der Beispiel-App wird so konfiguriert, dass der
HttpClient
keinen Umleitungen folgt. Wie weiter unten im Abschnitt Pseudoauthentifizierung erläutert wird, können Tests so das Ergebnis der ersten Reaktion der App überprüfen. In vielen dieser Tests mit einemLocation
-Header ist die erste Antwort eine Umleitung.Ein typischer Test verwendet die
HttpClient
- und die Hilfsprogrammmethoden, um die Anforderung und die Antwort zu verarbeiten:[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); }
Jede POST-Anforderung an das GS muss die Fälschungsschutzprüfung bestehen, die automatisch vom Daten- und Fälschungsschutzsystem der App durchgeführt wird. Als Vorbereitung auf die POST-Anforderung eines Tests muss die Test-App folgende Schritte ausführen:
- Senden einer Anforderung für die Seite.
- Analysieren des Fälschungsschutzcookies und des Anforderungsvalidierungstokens von der Antwort.
- Senden der POST-Anforderung mit vorhandenem Fälschungsschutzcookie und Anforderungsvalidierungstoken.
Die SendAsync
-Hilfsprogrammerweiterungsmethoden (Helpers/HttpClientExtensions.cs
) und die GetDocumentAsync
-Hilfsprogrammmethode (Helpers/HtmlHelpers.cs
) in der Beispiel-App verwenden den AngleSharp-Parser, um die Fälschungsschutzprüfungen mit den folgenden Methoden durchzuführen:
GetDocumentAsync
: empfängt HttpResponseMessage und gibtIHtmlDocument
zurückGetDocumentAsync
verwendet eine Factory, die eine virtuelle Antwort basierend auf der ursprünglichenHttpResponseMessage
vorbereitet. Weitere Informationen finden Sie in der AngleSharp-Dokumentation.SendAsync
-Erweiterungsmethoden fürHttpClient
erstellen eine HttpRequestMessage-Klasse und rufen SendAsync(HttpRequestMessage) auf, um Anforderungen an das GS zu übermitteln. Überladungen fürSendAsync
akzeptieren das HTML-Formular (IHtmlFormElement
) und Folgendes:- Schaltfläche „Senden“ des Formulars (
IHtmlElement
) - Formularwerteauflistung (
IEnumerable<KeyValuePair<string, string>>
) - Schaltfläche „Senden“ (
IHtmlElement
) und Formularwerte (IEnumerable<KeyValuePair<string, string>>
)
- Schaltfläche „Senden“ des Formulars (
Hinweis
AngleSharp ist eine Drittanbieter-Analysebibliothek, die in diesem Thema und in der Beispiel-App zu Demonstrationszwecken verwendet wird. AngleSharp wird für Integrationstests von ASP.NET Core-Apps weder unterstützt noch benötigt. Andere Parser können verwendet werden, beispielsweise Html Agility Pack (HAP). Ein anderer Ansatz besteht darin, Code zu schreiben, der das Anforderungsüberprüfungstoken und das Fälschungsschutzcookie des Fälschungsschutzsystems direkt verarbeitet.
Hinweis
Der In-Memory-Datenbankanbieter von EF Core kann für begrenzte und grundlegende Tests verwendet werden, der SQLite-Anbieter ist jedoch die empfohlene Option für In-Memory-Tests.
Anpassen des Clients mit WithWebHostBuilder
Wenn eine zusätzliche Konfiguration innerhalb einer Testmethode erforderlich ist, erstellt WithWebHostBuilder eine neue WebApplicationFactory
-Klasse mit einer IWebHostBuilder-Schnittstelle, die weiter konfiguriert wird.
Die Post_DeleteMessageHandler_ReturnsRedirectToRoot
-Testmethode der Beispiel-App zeigt die Verwendung von WithWebHostBuilder
. Bei diesem Test wird eine Datensatzlöschung in der Datenbank durch Auslösen einer Formularübermittlung im GS durchführt.
Da ein anderer Test in der IndexPageTests
-Klasse einen Vorgang durchführt, der alle Datensätze in der Datenbank löscht und der möglicherweise vor der Post_DeleteMessageHandler_ReturnsRedirectToRoot
-Methode ausgeführt wird, wird in dieser Testmethode ein erneutes Seeding der Datenbank durchgeführt, um sicherzustellen, dass ein Datensatz vorhanden ist, den das GS löschen kann. Die Auswahl der ersten Löschschaltfläche des messages
-Formulars im GS wird in der Anforderung an das GS simuliert:
[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);
}
Clientoptionen
In der folgenden Tabelle werden die verfügbaren Standardwerte für WebApplicationFactoryClientOptions beim Erstellen von HttpClient
-Instanzen aufgeführt.
Option | BESCHREIBUNG | Standard |
---|---|---|
AllowAutoRedirect | Ruft ab oder legt fest, ob HttpClient -Instanzen automatisch Umleitungsantworten befolgen sollen. |
true |
BaseAddress | Ruft die Basisadresse der HttpClient -Instanzen ab oder legt sie fest. |
http://localhost |
HandleCookies | Ruft ab oder legt fest, ob HttpClient -Instanzen Cookies verarbeiten sollen. |
true |
MaxAutomaticRedirections | Ruft die maximale Anzahl von Umleitungsantworten ab, die von HttpClient -Instanzen befolgt werden sollen, oder legt diese fest. |
7 |
Erstellen Sie die Klasse WebApplicationFactoryClientOptions
, und übergeben Sie sie an die Methode CreateClient() (Standardwerte im Codebeispiel):
// 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);
Fügen Sie Pseudodienste ein
Dienste können in einem Test überschrieben werden, indem ConfigureTestServices im Host-Generator aufgerufen wird. Um Pseudodienste einzufügen, muss das GS über eine Startup
-Klasse mit einer Startup.ConfigureServices
-Methode verfügen.
Das Beispiel-GS enthält einen bereichsbezogenen Dienst, der ein Zitat zurückgibt. Wenn die Indexseite angefordert wird, wird das Zitat in ein ausgeblendetes Feld auf der Indexseite eingebettet.
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">
Das folgende Markup wird generiert, wenn die GS-App ausgeführt wird:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
Um den Dienst und die Zitateinfügung in einem Integrationstest zu testen, wird vom Test ein Pseudodienst in das GS eingefügt. Der Pseudodienst ersetzt QuoteService
der App durch einen Dienst namens TestQuoteService
, der von der Test-App bereitgestellt wird:
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
wird aufgerufen, und der bereichsbezogene Dienst wird registriert:
[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);
}
Das während der Ausführung des Tests erstellte Markup gibt das von TestQuoteService
bereitgestellte Zitat wieder, somit ist die Assertion erfolgreich:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Pseudo-Authentifizierung
Tests in der AuthTests
-Klasse prüfen, ob ein sicherer Endpunkt:
- Einen nicht authentifizierten Benutzer an die Anmeldeseite der App zurückleitet.
- Den Inhalt für einen authentifizierten Benutzer zurückgibt.
Im GS verwendet die Seite /SecurePage
die Konvention AuthorizePage, um AuthorizeFilter auf die Seite anzuwenden. Weitere Informationen finden Sie unter Razor Pages-Autorisierungskonventionen.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Im Test Get_SecurePageRedirectsAnUnauthenticatedUser
wird WebApplicationFactoryClientOptions so eingestellt, dass Umleitungen nicht zulässig sind. Hierfür wird AllowAutoRedirect auf false
festgelegt:
[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);
}
Indem dem Client untersagt wird, die Umleitung zu befolgen, können folgende Prüfungen durchgeführt werden:
- Der vom GS zurückgegebene Statuscode kann mit dem erwarteten Ergebnis für HttpStatusCode.Redirect verglichen werden, anstatt mit dem endgültigen Statuscode nach der Umleitung zur Anmeldeseite (HttpStatusCode.OK).
- Der Wert für den
Location
-Header in den Antwortheadern wird geprüft, um zu bestätigen, dass er mithttp://localhost/Identity/Account/Login
beginnt. (Es wird nicht die abschließende Antwort der Anmeldeseite verwendet, bei der derLocation
-Header nicht vorhanden wäre.)
Die Test-App kann AuthenticationHandler<TOptions> in ConfigureTestServices simulieren, um Aspekte der Authentifizierung und Autorisierung zu testen. Ein minimales Szenario gibt AuthenticateResult.Success zurück:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "Test");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
wird aufgerufen, um einen Benutzer zu authentifizieren, wenn das Authentifizierungsschema auf Test
festgelegt wird, in dem AddAuthentication
für ConfigureTestServices
registriert ist. Es ist wichtig, dass das Test
-Schema mit dem Schema übereinstimmt, das Ihre App erwartet. Andernfalls funktioniert die Authentifizierung nicht.
[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);
}
Weitere Informationen zu WebApplicationFactoryClientOptions
finden Sie im Abschnitt Clientoptionen.
Festlegen der Umgebung
Standardmäßig ist die Host- und App-Umgebung des GS für die Verwendung der Entwicklungsumgebung konfiguriert. So überschreiben Sie die Umgebung des GS bei Verwendung von IHostBuilder
:
- Legen Sie die
ASPNETCORE_ENVIRONMENT
-Umgebungsvariable fest (z. B.Staging
,Production
oder ein anderer benutzerdefinierter Wert wieTesting
). - Überschreiben Sie
CreateHostBuilder
in der Test-App, um Umgebungsvariablen mit dem PräfixASPNETCORE
zu lesen.
protected override IHostBuilder CreateHostBuilder() =>
base.CreateHostBuilder()
.ConfigureHostConfiguration(
config => config.AddEnvironmentVariables("ASPNETCORE"));
Wenn das GS den Webhost (IWebHostBuilder
) verwendet, überschreiben Sie CreateWebHostBuilder
:
protected override IWebHostBuilder CreateWebHostBuilder() =>
base.CreateWebHostBuilder().UseEnvironment("Testing");
Ableitung des Inhaltsstammpfads der App durch die Testinfrastruktur
Der Konstruktor WebApplicationFactory
leitet den Inhaltsstammpfad der App ab, indem er in der Assembly, die die Integrationstests enthält, nach einer WebApplicationFactoryContentRootAttribute-Klasse mit einem Schlüssel sucht, der der TEntryPoint
-Assembly System.Reflection.Assembly.FullName
entspricht. Wenn kein Attribut mit dem richtigen Schlüssel gefunden wird, greift WebApplicationFactory
auf die Suche nach einer Projektmappendatei ( .sln) zurück und fügt den TEntryPoint
-Assemblynamen an das Projektmappenverzeichnis an. Das Stammverzeichnis der App (der Inhaltsstammpfad) wird verwendet, um Sichten und Inhaltsdateien zu ermitteln.
Deaktivieren der Erstellung von Schattenkopien
Das Erstellen von Schattenkopien bewirkt, dass die Tests in einem anderen Verzeichnis als dem Ausgabeverzeichnis ausgeführt werden. Wenn Ihre Tests auf dem Laden von Dateien relativ zu Assembly.Location
basieren und Probleme auftreten, müssen Sie möglicherweise das Schattenkopiervorgang deaktivieren.
Um das Schattenkopieren bei Verwendung von xUnit zu deaktivieren, erstellen Sie eine xunit.runner.json
-Datei in Ihrem Testprojektverzeichnis mit der richtigen Konfigurationseinstellung:
{
"shadowCopy": false
}
Verwerfen von Objekten
Nachdem die Tests der IClassFixture
-Implementierung abgeschlossen sind, werden TestServer und HttpClient verworfen, wenn xUnit WebApplicationFactory
verwirft. Wenn vom Entwickler instanziierte Objekte verworfen werden müssen, müssen Sie dies in der IClassFixture
-Implementierung tun. Weitere Informationen finden Sie unter Implementieren einer Dispose-Methode.
Beispiel für Integrationstests
Die Beispiel-App besteht aus zwei Apps:
App | Projektverzeichnis | Beschreibung |
---|---|---|
Nachrichten-App (das GS) | src/RazorPagesProject |
Ermöglicht einem Benutzer, Nachrichten hinzuzufügen, eine oder alle Nachrichten zu löschen und Nachrichten zu analysieren. |
Testen der App | tests/RazorPagesProject.Tests |
Wird für den Integrationstest des GS verwendet. |
Die Tests können mit den integrierten Testfunktionen einer IDE, wie z. B. Visual Studio ausgeführt werden. Wenn Sie Visual Studio Code oder die Befehlszeile verwenden, führen Sie den folgenden Befehl über eine Eingabeaufforderung im Verzeichnis tests/RazorPagesProject.Tests
aus:
dotnet test
Organisation der Nachrichten-App (GS)
Beim GS handelt es sich um ein Razor Pages-Nachrichtensystem mit folgenden Merkmalen:
- Die Indexseite der App (
Pages/Index.cshtml
undPages/Index.cshtml.cs
) stellt eine Benutzeroberfläche und Seitenmodellmethoden bereit, mit denen Sie das Hinzufügen, Löschen und Analysieren von Nachrichten (durchschnittliche Anzahl von Wörtern pro Nachricht) steuern können. - Eine Nachricht wird von der
Message
-Klasse (Data/Message.cs
) mit zwei Eigenschaften beschrieben:Id
(Schlüssel) undText
(Nachricht). DieText
-Eigenschaft ist erforderlich und auf 200 Zeichen beschränkt. - Nachrichten werden mithilfe der In-Memory-Datenbank von Entity Framework† gespeichert.
- Die App enthält eine Datenzugriffsebene (DAL) in ihrer Datenbankkontextklasse
AppDbContext
(Data/AppDbContext.cs
). - Wenn die Datenbank beim Starten der App leer ist, wird der Nachrichtenspeicher mit drei Nachrichten initialisiert.
- Die App enthält eine
/SecurePage
, auf die nur ein authentifizierter Benutzer zugreifen kann.
†Im Entity Framework-Thema Testen mit InMemory wird die Verwendung einer In-Memory-Datenbank für Tests mit MSTest erläutert. In diesem Thema wird das Testframework xUnit verwendet. Testkonzepte und Testimplementierungen in verschiedenen Testframeworks sind ähnlich, jedoch nicht identisch.
Obwohl die App nicht das Repositorymuster verwendet und kein effektives Beispiel für das Arbeitseinheitsmuster ist, unterstützt Razor Pages diese Entwicklungsmuster. Weitere Informationen finden Sie unter Entwerfen der Persistenzebene der Infrastruktur und Testcontrollerlogik (im Beispiel wird das Repositorymuster implementiert).
Organisation der Test-App
Die Test-App ist eine Konsolen-App im Verzeichnis tests/RazorPagesProject.Tests
.
Test-App-Verzeichnis | Beschreibung |
---|---|
AuthTests |
Enthält Testmethoden für Folgendes:
|
BasicTests |
Enthält eine Testmethode für Routing und Inhaltstyp. |
IntegrationTests |
Enthält die Integrationstests für die Indexseite unter Verwendung der benutzerdefinierten WebApplicationFactory -Klasse. |
Helpers/Utilities |
|
Das Testframework ist xUnit. Integrationstests werden mit der Klasse Microsoft.AspNetCore.TestHost durchgeführt, die TestServer enthält. Da das Paket Microsoft.AspNetCore.Mvc.Testing
zum Konfigurieren des Testhosts und des Testservers verwendet wird, benötigen die Pakete TestHost
und TestServer
keine direkten Paketverweise in der Projektdatei der Test-App bzw. keine Entwicklerkonfiguration in der Test-App.
Für Integrationstests muss die Datenbank in der Regel vor der Testausführung ein kleines Dataset enthalten. Beispielsweise wird bei einem Löschtest ein Löschvorgang eines Datensatzes der Datenbank abgerufen, weshalb die Datenbank mindestens einen Datensatz aufweisen muss, damit die Löschanforderung erfolgreich ausgeführt wird.
Die Beispiel-App führt ein Seeding der Datenbank mit drei Nachrichten in Utilities.cs
durch, die von Tests bei der Ausführung verwendet werden können:
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." }
};
}
Der Datenbankkontext des GS wird in dessen Startup.ConfigureServices
-Methode registriert. Der builder.ConfigureServices
-Rückruf der Test-App wird ausgeführt, nachdem der Startup.ConfigureServices
-Code der App ausgeführt wurde. Um eine andere Datenbank für die Tests zu verwenden, muss der Datenbankkontext der App in builder.ConfigureServices
ersetzt werden. Weitere Informationen finden Sie im Abschnitt Anpassen von WebApplicationFactory.
Für GS, die weiterhin den Webhost verwenden, wird der builder.ConfigureServices
-Rückruf der Test-App ausgeführt, bevor der Startup.ConfigureServices
-Code des GS ausgeführt wird. Der builder.ConfigureTestServices
-Rückruf der Test-App wird danach ausgeführt.
Zusätzliche Ressourcen
Dieser Artikel setzt Grundkenntnisse zu Komponententests voraus. Wenn Sie nicht mit Testkonzepten vertraut sind, lesen Sie den Artikel Komponententests in .NET Core und .NET Standard und zugehörige Inhalte.
Anzeigen oder Herunterladen von Beispielcode (Vorgehensweise zum Herunterladen)
Bei der Beispiel-App handelt es sich um eine Razor Pages-App, hierfür werden grundlegende Kenntnisse über Razor Pages vorausgesetzt. Wenn Sie nicht mit Razor Pages vertraut sind, lesen Sie die folgenden Artikel:
Zum Testen von Single-Page-Webanwendungen empfiehlt sich ein Tool wie Playwright für .NET, mit dem ein Browser automatisiert werden kann.
Einführung in Integrationstests
Integrationstests bewerten die Komponenten einer App auf breiterer Ebene als Komponententests. Komponententests werden verwendet, um isolierte Softwarekomponenten wie z. B. einzelne Klassenmethoden zu testen. Integrationstests bestätigen, dass zwei oder mehr App-Komponenten zusammenarbeiten, um ein erwartetes Ergebnis zu erzielen, ggf. auch unter Einbindung aller Komponenten, die für die vollständige Verarbeitung einer Anforderung erforderlich sind.
Diese umfassenderen Tests werden verwendet, um die Infrastruktur und das gesamte Framework der App zu testen, häufig einschließlich der folgenden Komponenten:
- Datenbank
- Dateisystem
- Netzwerk-Appliances
- Anforderung/Antwort-Pipeline
Komponententests verwenden anstelle von Infrastrukturkomponenten künstliche Komponenten, die als Fakes oder Pseudoobjekte bezeichnet werden.
Im Gegensatz zu Komponententests gilt für Integrationstests:
- Sie verwenden die tatsächlichen Komponenten, die von der App in der Produktionsumgebung verwendet werden.
- Sie erfordern mehr Code und Datenverarbeitung.
- Ihre Ausführung dauert länger.
Beschränken Sie daher die Verwendung von Integrationstests auf die wichtigsten Infrastrukturszenarios. Wenn ein Verhalten mithilfe eines Komponententests oder eines Integrationstests getestet werden kann, wählen Sie den Komponententest.
Bei der Besprechung von Integrationstests wird das getestete Projekt häufig als getestetes System oder kurz GS (englisch System Under Test, SUT) bezeichnet. In diesem Artikel wird zum Verweis auf die zu testende ASP.NET Core-Anwendung der Begriff „GS“ verwendet.
Schreiben Sie keine Integrationstests für jede Permutation des Daten- und Dateizugriffs bei Datenbanken und Dateisystemen. Unabhängig davon, wie viele Elemente in einer App mit Datenbanken und Dateisystemen interagieren, ist ein fokussierter Satz von Lese-, Schreib-, Update- und Lösch-Integrationstests üblicherweise in der Lage, die Datenbank- und Dateisystemkomponenten adäquat zu testen. Verwenden Sie Komponententests für Routinetests der Methodenlogik, die mit diesen Komponenten interagieren. Bei Komponententests beschleunigt die Verwendung von Fake- oder Pseudoergebnissen für eine Infrastruktur die Testausführung.
Integrationstests in ASP.NET Core
Integrationstests in ASP.NET Core erfordern Folgendes:
- Es wird ein Testprojekt verwendet, um die Tests einzugrenzen und auszuführen. Das Testprojekt enthält einen Verweis auf das GS.
- Das Testprojekt erstellt einen Testwebhost für das GS und verwendet einen Testserverclient, um Anforderungen und Antworten im Zusammenhang mit dem GS zu verarbeiten.
- Um die Tests auszuführen und die Testergebnisse zu melden, wird ein Test-Runner verwendet.
Integrationstests folgen einer Sequenz von Ereignissen, die die üblichen Testschritte Arrange, Act und Assert umfassen:
- Der Webhost des GS wird konfiguriert.
- Es wird ein Testserverclient erstellt, um Anforderungen an die App zu senden.
- Der Testschritt Arrange wird ausgeführt: Die Test-App bereitet eine Anforderung vor.
- Der Testschritt Act wird ausgeführt: Der Client sendet die Anforderung und empfängt die Antwort.
- Der Testschritt Assert wird ausgeführt: Die tatsächliche Antwort wird je nach der erwarteten Antwort als Pass oder Fail bewertet.
- Der Prozess wird so lange fortgesetzt, bis alle Tests ausgeführt wurden.
- Die Testergebnisse werden gemeldet.
Üblicherweise ist der Testwebhost anders konfiguriert als der normale Webhost der App für die Testläufe. Beispielsweise könnten für die Tests eine andere Datenbank oder andere App-Einstellungen verwendet werden.
Infrastrukturkomponenten wie der Testwebhost und der In-Memory-Testserver (TestServer) werden durch das Paket Microsoft.AspNetCore.Mvc.Testing bereitgestellt oder verwaltet. Durch die Verwendung dieses Pakets werden die Testerstellung und -ausführung optimiert.
Das Microsoft.AspNetCore.Mvc.Testing
-Paket verarbeitet die folgenden Aufgaben:
- Es kopiert die Datei für Abhängigkeiten (
.deps
) aus dem GS in dasbin
Verzeichnis des Testprojekts. - Es legt das Inhaltsstammelement auf das Projektstammelement des GS fest, damit statische Dateien und Seiten/Ansichten bei der Ausführung der Tests gefunden werden.
- Es stellt die Klasse WebApplicationFactory zur Optimierung des Bootstrappings des GS mit
TestServer
bereit.
In der Dokumentation der Komponententests wird beschrieben, wie Sie ein Testprojekt und einen Test-Runner einrichten. Ferner finden Sie dort ausführliche Anweisungen zum Ausführen von Tests sowie Empfehlungen zum Benennen von Tests und Testklassen.
Unterteilen Sie Komponententests und Integrationstests in verschiedene Projekte. Trennen der Tests:
- Damit wird sichergestellt, dass Komponenten für Infrastrukturtests nicht versehentlich in die Komponententests eingeschlossen werden.
- So können Sie steuern, welche Testsätze ausgeführt werden.
Es gibt praktisch keinen Unterschied zwischen der Konfiguration für Tests von Razor Pages-Apps und MVC-Apps. Der einzige Unterschied besteht darin, wie die Tests benannt werden. In einer Razor Pages-App werden Tests von Seitenendpunkten normalerweise nach der Seitenmodellklasse benannt (z. B. IndexPageTests
für das Testen der Komponentenintegration für die Indexseite). In einer MVC-App werden Tests in der Regel nach Controllerklassen organisiert und nach den von ihnen getesteten Controllern benannt (z. B. HomeControllerTests
für das Testen der Komponentenintegration für den Home-Controller).
Voraussetzungen für Test-Apps
Für das Testprojekt muss Folgendes erfüllt sein:
- Verweisen Sie auf das
Microsoft.AspNetCore.Mvc.Testing
-Paket. - Geben Sie das Web SDK in der Projektdatei an (
<Project Sdk="Microsoft.NET.Sdk.Web">
).
Diese Voraussetzungen können Sie in der Beispiel-App sehen. Sehen Sie sich die Datei tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj
an. Die Beispiel-App verwendet das xUnit-Testframework und die AngleSharp-Parserbibliothek, sodass die Beispiel-APP auch auf Folgendes verweist:
In Apps, die Version 2.4.2 oder höher von xunit.runner.visualstudio
verwenden, muss das Testprojekt auf das Paket Microsoft.NET.Test.Sdk
verweisen.
Entity Framework Core wird ebenfalls in den Tests verwendet. Weitere Informationen finden Sie auch in der Projektdatei auf GitHub.
GS-Umgebung
Wenn die Umgebung des GS nicht festgelegt ist, wird standardmäßig die Entwicklungsumgebung verwendet.
Grundlegende Tests mit der Standard-WebApplicationFactory
Machen Sie die implizit definierte Program
-Klasse auf eine der folgenden Arten für das Testprojekt verfügbar:
Machen Sie interne Typen aus der Web-App für das Testprojekt verfügbar. Dies ist in der Projektdatei (
.csproj
) für das GS möglich:<ItemGroup> <InternalsVisibleTo Include="MyTestProject" /> </ItemGroup>
Legen Sie die
Program
-Klasse mithilfe einer partiellen Klassendeklaration als öffentlich fest:var builder = WebApplication.CreateBuilder(args); // ... Configure services, routes, etc. app.Run(); + public partial class Program { }
Die Beispiel-App verfolgt einen Ansatz mit der partiellen
Program
-Klasse.
WebApplicationFactory<TEntryPoint> wird verwendet, um eine TestServer-Klasse für die Integrationstests zu erstellen. TEntryPoint
ist die Einstiegspunktklasse des GS, in der Regel Program.cs
.
Testklassen implementieren eine Klassenfixture-Schnittstelle (IClassFixture
), um anzugeben, dass die Klasse Tests enthält und um gemeinsame Objektinstanzen in den Tests in der Klasse bereitzustellen.
Die folgende Testklasse (BasicTests
) verwendet die WebApplicationFactory
für den Bootstrap des GS und um eine HttpClient-Klasse für die Testmethode Get_EndpointsReturnSuccessAndCorrectContentType
bereitzustellen. Die Methode prüft, ob die Antwort einen erfolgreichen Statuscode (200–299) enthält und der Content-Type
-Header für mehrere App-Seiten text/html; charset=utf-8
lautet.
CreateClient() erstellt eine Instanz von HttpClient
, die automatisch Umleitungen folgt und Cookies verarbeitet.
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());
}
}
Standardmäßig werden nicht erforderliche Cookies nicht über Anforderungen hinweg beibehalten, wenn die Einwilligungsrichtlinie zur Datenschutz-Grundverordnung (DSGVO) aktiviert ist. Um nicht erforderliche Cookies beizubehalten, wie z. B. diejenigen, die vom TempData-Anbieter verwendet werden, markieren Sie die Cookies in den Tests als unverzichtbar. Anweisungen zum Markieren eines Cookies als erforderlich finden Sie unter Erforderliche Cookies.
AngleSharp oder Application Parts
für Antifälschungsüberprüfungen
In diesem Artikel wird der AngleSharp-Parser für die Antifälschungsüberprüfungen verwendet, indem Seiten geladen werden und der HTML-Code analysiert wird. Wenn Sie die Endpunkte des Controllers und die Besuche von Razor-Seiten auf niedrigerer Ebene testen möchten, ohne dabei das Rendern im Browser beachten zu müssen, sollten Sie die Verwendung von Application Parts
in Erwägung ziehen. Beim Ansatz mit Anwendungsteilen wird ein Controller oder eine Razor-Seite in die App injiziert, mit dem oder der JSON-Anforderungen erstellt werden können, um die erforderlichen Werte abzurufen. Weitere Informationen finden Sie im Blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts (Integrationstests bei ASP.NET Core-Ressourcen mit Fälschungsschutz durch Anwendungsteile) und dem zugehörigen GitHub-Repository von Martin Costello.
Anpassen von WebApplicationFactory
Die Webhostkonfiguration kann unabhängig von den Testklassen durch Erben von der WebApplicationFactory<TEntryPoint> erstellt werden, um eine oder mehrere benutzerdefinierte Factorys zu erstellen:
Führen Sie eine Vererbung von
WebApplicationFactory
durch, und überschreiben Sie ConfigureWebHost. Der IWebHostBuilder ermöglicht die Konfiguration der Serversammlung mitIWebHostBuilder.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"); } }
Das Datenbankseeding in der Beispiel-App wird mithilfe der
InitializeDbForTests
-Methode durchgeführt. Die Methode wird im Abschnitt Beispiel für Integrationstests: Organisation der Test-App beschrieben.Der Datenbankkontext des GS wird in
Program.cs
registriert. Derbuilder.ConfigureServices
-Rückruf der Test-App wird ausgeführt, nachdem derProgram.cs
-Code der App ausgeführt wurde. Um für die Tests eine andere Datenbank als die Datenbank der App zu verwenden, muss der Datenbankkontext der App inbuilder.ConfigureServices
ersetzt werden.Die Beispiel-App findet den Dienstdeskriptor für den Datenbankkontext und verwendet den Deskriptor, um die Dienstregistrierung zu entfernen. Anschließend fügt die Factory einen neuen
ApplicationDbContext
hinzu, der eine Datenbank im Arbeitsspeicher für die Tests verwendet.Wenn Sie eine Verbindung mit einer anderen Datenbank herstellen möchten, ändern Sie die
DbConnection
. So verwenden Sie eine SQL Server-Testdatenbank:
- Verweisen Sie in der Projektdatei auf das NuGet-Paket
Microsoft.EntityFrameworkCore.SqlServer
. - Rufen Sie
UseInMemoryDatabase
auf.
Verwenden Sie die benutzerdefinierte
CustomWebApplicationFactory
in Testklassen. Das folgende Beispiel verwendet die Factory in derIndexPageTests
-Klasse: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 }); }
Der Client der Beispiel-App wird so konfiguriert, dass der
HttpClient
keinen Umleitungen folgt. Wie weiter unten im Abschnitt Pseudoauthentifizierung erläutert wird, können Tests so das Ergebnis der ersten Reaktion der App überprüfen. In vielen dieser Tests mit einemLocation
-Header ist die erste Antwort eine Umleitung.Ein typischer Test verwendet die
HttpClient
- und die Hilfsprogrammmethoden, um die Anforderung und die Antwort zu verarbeiten:[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); }
Jede POST-Anforderung an das GS muss die Fälschungsschutzprüfung bestehen, die automatisch vom Daten- und Fälschungsschutzsystem der App durchgeführt wird. Als Vorbereitung auf die POST-Anforderung eines Tests muss die Test-App folgende Schritte ausführen:
- Senden einer Anforderung für die Seite.
- Analysieren des Fälschungsschutzcookies und des Anforderungsvalidierungstokens von der Antwort.
- Senden der POST-Anforderung mit vorhandenem Fälschungsschutzcookie und Anforderungsvalidierungstoken.
Die SendAsync
-Hilfsprogrammerweiterungsmethoden (Helpers/HttpClientExtensions.cs
) und die GetDocumentAsync
-Hilfsprogrammmethode (Helpers/HtmlHelpers.cs
) in der Beispiel-App verwenden den AngleSharp-Parser, um die Fälschungsschutzprüfungen mit den folgenden Methoden durchzuführen:
GetDocumentAsync
: empfängt HttpResponseMessage und gibtIHtmlDocument
zurückGetDocumentAsync
verwendet eine Factory, die eine virtuelle Antwort basierend auf der ursprünglichenHttpResponseMessage
vorbereitet. Weitere Informationen finden Sie in der AngleSharp-Dokumentation.SendAsync
-Erweiterungsmethoden fürHttpClient
erstellen eine HttpRequestMessage-Klasse und rufen SendAsync(HttpRequestMessage) auf, um Anforderungen an das GS zu übermitteln. Überladungen fürSendAsync
akzeptieren das HTML-Formular (IHtmlFormElement
) und Folgendes:- Schaltfläche „Senden“ des Formulars (
IHtmlElement
) - Formularwerteauflistung (
IEnumerable<KeyValuePair<string, string>>
) - Schaltfläche „Senden“ (
IHtmlElement
) und Formularwerte (IEnumerable<KeyValuePair<string, string>>
)
- Schaltfläche „Senden“ des Formulars (
AngleSharp ist eine Drittanbieter-Analysebibliothek, die in diesem Artikel und in der Beispiel-App zu Demonstrationszwecken verwendet wird. AngleSharp wird für Integrationstests von ASP.NET Core-Apps weder unterstützt noch benötigt. Andere Parser können verwendet werden, beispielsweise Html Agility Pack (HAP). Ein anderer Ansatz besteht darin, Code zu schreiben, der das Anforderungsüberprüfungstoken und das Fälschungsschutzcookie des Fälschungsschutzsystems direkt verarbeitet. Weitere Informationen finden Sie unter AngleSharp oder Application Parts
für Antifälschungsüberprüfungen in diesem Artikel.
Der In-Memory-Datenbankanbieter von EF Core kann für begrenzte und grundlegende Tests verwendet werden, der SQLite-Anbieter ist jedoch die empfohlene Option für Tests im Arbeitsspeicher.
Weitere Informationen finden Sie unter Erweitern des Startvorgangs mit Startfiltern. Darin wird gezeigt, wie Middleware mithilfe von IStartupFilter konfiguriert wird. Das kann nützlich sein, wenn für einen Test ein benutzerdefinierter Dienst oder eine benutzerdefinierte Middleware erforderlich ist.
Anpassen des Clients mit WithWebHostBuilder
Wenn eine zusätzliche Konfiguration innerhalb einer Testmethode erforderlich ist, erstellt WithWebHostBuilder eine neue WebApplicationFactory
-Klasse mit einer IWebHostBuilder-Schnittstelle, die weiter konfiguriert wird.
Der Beispielcode ruft WithWebHostBuilder
auf, um konfigurierte Dienste durch Test-Stubs zu ersetzen. Weitere Informationen und Beispielnutzung finden Sie in dem Artikel Injizieren von Pseudodiensten.
Die Post_DeleteMessageHandler_ReturnsRedirectToRoot
-Testmethode der Beispiel-App zeigt die Verwendung von WithWebHostBuilder
. Bei diesem Test wird eine Datensatzlöschung in der Datenbank durch Auslösen einer Formularübermittlung im GS durchführt.
Da ein anderer Test in der IndexPageTests
-Klasse einen Vorgang durchführt, der alle Datensätze in der Datenbank löscht und der möglicherweise vor der Post_DeleteMessageHandler_ReturnsRedirectToRoot
-Methode ausgeführt wird, wird in dieser Testmethode ein erneutes Seeding der Datenbank durchgeführt, um sicherzustellen, dass ein Datensatz vorhanden ist, den das GS löschen kann. Die Auswahl der ersten Löschschaltfläche des messages
-Formulars im GS wird in der Anforderung an das GS simuliert:
[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);
}
Clientoptionen
Auf der WebApplicationFactoryClientOptions-Seite finden Sie die Standardeinstellungen und verfügbaren Optionen für das Erstellen von HttpClient
-Instanzen.
Erstellen Sie die WebApplicationFactoryClientOptions
-Klasse, und übergeben Sie sie an die CreateClient()-Methode:
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
});
}
HINWEIS: Um HTTPS-Umleitungswarnungen in Protokollen bei Verwendung von HTTPS-Umleitungsmiddleware zu vermeiden, legen Sie die Einstellung BaseAddress = new Uri("https://localhost")
fest.
Fügen Sie Pseudodienste ein
Dienste können in einem Test überschrieben werden, indem ConfigureTestServices im Host-Generator aufgerufen wird. Um die Außerkraftsetzungsdienste auf den Test selbst zu beschränken, wird die WithWebHostBuilder-Methode zum Abrufen eines Host-Generators verwendet. Dies kann in den folgenden Tests zu sehen sein:
- Get_QuoteService_ProvidesQuoteInPage
- Get_GithubProfilePageCanGetAGithubUser
- Get_SecurePageIsReturnedForAnAuthenticatedUser
Das Beispiel-GS enthält einen bereichsbezogenen Dienst, der ein Zitat zurückgibt. Wenn die Indexseite angefordert wird, wird das Zitat in ein ausgeblendetes Feld auf der Indexseite eingebettet.
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">
Das folgende Markup wird generiert, wenn die GS-App ausgeführt wird:
<input id="quote" type="hidden" value="Come on, Sarah. We've an appointment in
London, and we're already 30,000 years late.">
Um den Dienst und die Zitateinfügung in einem Integrationstest zu testen, wird vom Test ein Pseudodienst in das GS eingefügt. Der Pseudodienst ersetzt QuoteService
der App durch einen Dienst namens TestQuoteService
, der von der Test-App bereitgestellt wird:
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
wird aufgerufen, und der bereichsbezogene Dienst wird registriert:
[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);
}
Das während der Ausführung des Tests erstellte Markup gibt das von TestQuoteService
bereitgestellte Zitat wieder, somit ist die Assertion erfolgreich:
<input id="quote" type="hidden" value="Something's interfering with time,
Mr. Scarman, and time is my business.">
Pseudo-Authentifizierung
Tests in der AuthTests
-Klasse prüfen, ob ein sicherer Endpunkt:
- Nicht authentifizierte Benutzer*innen auf die Anmeldeseite der App umleitet
- Den Inhalt für einen authentifizierten Benutzer zurückgibt.
Im GS verwendet die Seite /SecurePage
die Konvention AuthorizePage, um AuthorizeFilter auf die Seite anzuwenden. Weitere Informationen finden Sie unter Razor Pages-Autorisierungskonventionen.
services.AddRazorPages(options =>
{
options.Conventions.AuthorizePage("/SecurePage");
});
Im Test Get_SecurePageRedirectsAnUnauthenticatedUser
wird WebApplicationFactoryClientOptions so eingestellt, dass Umleitungen nicht zulässig sind. Hierfür wird AllowAutoRedirect auf false
festgelegt:
[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);
}
Indem dem Client untersagt wird, die Umleitung zu befolgen, können folgende Prüfungen durchgeführt werden:
- Der vom GS zurückgegebene Statuscode kann mit dem erwarteten Ergebnis für HttpStatusCode.Redirect verglichen werden, anstatt mit dem endgültigen Statuscode nach der Umleitung zur Anmeldeseite (HttpStatusCode.OK).
- Der Wert für den
Location
-Header in den Antwortheadern wird geprüft, um zu bestätigen, dass er mithttp://localhost/Identity/Account/Login
beginnt. Es wird nicht die abschließende Antwort der Anmeldeseite verwendet, bei der derLocation
-Header nicht vorhanden wäre.
Die Test-App kann AuthenticationHandler<TOptions> in ConfigureTestServices simulieren, um Aspekte der Authentifizierung und Autorisierung zu testen. Ein minimales Szenario gibt AuthenticateResult.Success zurück:
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
var identity = new ClaimsIdentity(claims, "Test");
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, "TestScheme");
var result = AuthenticateResult.Success(ticket);
return Task.FromResult(result);
}
}
TestAuthHandler
wird aufgerufen, um einen Benutzer zu authentifizieren, wenn das Authentifizierungsschema auf TestScheme
festgelegt wird, in dem AddAuthentication
für ConfigureTestServices
registriert ist. Es ist wichtig, dass das TestScheme
-Schema mit dem Schema übereinstimmt, das Ihre App erwartet. Andernfalls funktioniert die Authentifizierung nicht.
[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);
}
Weitere Informationen zu WebApplicationFactoryClientOptions
finden Sie im Abschnitt Clientoptionen.
Grundlegende Tests für Middleware zur Authentifizierung
In diesem GitHub-Repository finden Sie grundlegende Tests für Middleware zur Authentifizierung. Es enthält einen Testserver, der für das Testszenario spezifisch ist.
Festlegen der Umgebung
Legen Sie die Umgebung in der benutzerdefinierten Anwendungsfactory fest:
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");
}
}
Ableitung des Inhaltsstammpfads der App durch die Testinfrastruktur
Der Konstruktor WebApplicationFactory
leitet den Inhaltsstammpfad der App ab, indem er in der Assembly, die die Integrationstests enthält, nach einer WebApplicationFactoryContentRootAttribute-Klasse mit einem Schlüssel sucht, der der TEntryPoint
-Assembly System.Reflection.Assembly.FullName
entspricht. Wenn kein Attribut mit dem richtigen Schlüssel gefunden wird, greift WebApplicationFactory
auf die Suche nach einer Projektmappendatei ( .sln) zurück und fügt den TEntryPoint
-Assemblynamen an das Projektmappenverzeichnis an. Das Stammverzeichnis der App (der Inhaltsstammpfad) wird verwendet, um Sichten und Inhaltsdateien zu ermitteln.
Deaktivieren der Erstellung von Schattenkopien
Das Erstellen von Schattenkopien bewirkt, dass die Tests in einem anderen Verzeichnis als dem Ausgabeverzeichnis ausgeführt werden. Wenn Ihre Tests auf dem Laden von Dateien relativ zu Assembly.Location
basieren und Probleme auftreten, müssen Sie möglicherweise das Schattenkopiervorgang deaktivieren.
Um das Schattenkopieren bei Verwendung von xUnit zu deaktivieren, erstellen Sie eine xunit.runner.json
-Datei in Ihrem Testprojektverzeichnis mit der richtigen Konfigurationseinstellung:
{
"shadowCopy": false
}
Verwerfen von Objekten
Nachdem die Tests der IClassFixture
-Implementierung abgeschlossen sind, werden TestServer und HttpClient verworfen, wenn xUnit WebApplicationFactory
verwirft. Wenn vom Entwickler instanziierte Objekte verworfen werden müssen, müssen Sie dies in der IClassFixture
-Implementierung tun. Weitere Informationen finden Sie unter Implementieren einer Dispose-Methode.
Beispiel für Integrationstests
Die Beispiel-App besteht aus zwei Apps:
App | Projektverzeichnis | Beschreibung |
---|---|---|
Nachrichten-App (das GS) | src/RazorPagesProject |
Ermöglicht einem Benutzer, Nachrichten hinzuzufügen, eine oder alle Nachrichten zu löschen und Nachrichten zu analysieren. |
Testen der App | tests/RazorPagesProject.Tests |
Wird für den Integrationstest des GS verwendet. |
Die Tests können mit den integrierten Testfunktionen einer IDE, wie z. B. Visual Studio ausgeführt werden. Wenn Sie Visual Studio Code oder die Befehlszeile verwenden, führen Sie den folgenden Befehl über eine Eingabeaufforderung im Verzeichnis tests/RazorPagesProject.Tests
aus:
dotnet test
Organisation der Nachrichten-App (GS)
Beim GS handelt es sich um ein Razor Pages-Nachrichtensystem mit folgenden Merkmalen:
- Die Indexseite der App (
Pages/Index.cshtml
undPages/Index.cshtml.cs
) stellt eine Benutzeroberfläche und Seitenmodellmethoden bereit, mit denen Sie das Hinzufügen, Löschen und Analysieren von Nachrichten (durchschnittliche Anzahl von Wörtern pro Nachricht) steuern können. - Eine Nachricht wird von der
Message
-Klasse (Data/Message.cs
) mit zwei Eigenschaften beschrieben:Id
(Schlüssel) undText
(Nachricht). DieText
-Eigenschaft ist erforderlich und auf 200 Zeichen beschränkt. - Nachrichten werden mithilfe der In-Memory-Datenbank von Entity Framework† gespeichert.
- Die App enthält eine Datenzugriffsebene (DAL) in ihrer Datenbankkontextklasse
AppDbContext
(Data/AppDbContext.cs
). - Wenn die Datenbank beim Starten der App leer ist, wird der Nachrichtenspeicher mit drei Nachrichten initialisiert.
- Die App enthält eine
/SecurePage
, auf die nur ein authentifizierter Benutzer zugreifen kann.
† Im EF-Thema Testen mit InMemory wird die Verwendung einer Datenbank im Arbeitsspeicher für Tests mit MSTest erläutert. In diesem Thema wird das Testframework xUnit verwendet. Testkonzepte und Testimplementierungen in verschiedenen Testframeworks sind ähnlich, jedoch nicht identisch.
Obwohl die App nicht das Repositorymuster verwendet und kein effektives Beispiel für das Arbeitseinheitsmuster ist, unterstützt Razor Pages diese Entwicklungsmuster. Weitere Informationen finden Sie unter Entwerfen der Persistenzebene der Infrastruktur und Testcontrollerlogik (im Beispiel wird das Repositorymuster implementiert).
Organisation der Test-App
Die Test-App ist eine Konsolen-App im Verzeichnis tests/RazorPagesProject.Tests
.
Test-App-Verzeichnis | Beschreibung |
---|---|
AuthTests |
Enthält Testmethoden für Folgendes:
|
BasicTests |
Enthält eine Testmethode für Routing und Inhaltstyp. |
IntegrationTests |
Enthält die Integrationstests für die Indexseite unter Verwendung der benutzerdefinierten WebApplicationFactory -Klasse. |
Helpers/Utilities |
|
Das Testframework ist xUnit. Integrationstests werden mit der Klasse Microsoft.AspNetCore.TestHost durchgeführt, die TestServer enthält. Da das Paket Microsoft.AspNetCore.Mvc.Testing
zum Konfigurieren des Testhosts und des Testservers verwendet wird, benötigen die Pakete TestHost
und TestServer
keine direkten Paketverweise in der Projektdatei der Test-App bzw. keine Entwicklerkonfiguration in der Test-App.
Für Integrationstests muss die Datenbank in der Regel vor der Testausführung ein kleines Dataset enthalten. Beispielsweise wird bei einem Löschtest ein Löschvorgang eines Datensatzes der Datenbank abgerufen, weshalb die Datenbank mindestens einen Datensatz aufweisen muss, damit die Löschanforderung erfolgreich ausgeführt wird.
Die Beispiel-App führt ein Seeding der Datenbank mit drei Nachrichten in Utilities.cs
durch, die von Tests bei der Ausführung verwendet werden können:
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." }
};
}
Der Datenbankkontext des GS wird in Program.cs
registriert. Der builder.ConfigureServices
-Rückruf der Test-App wird ausgeführt, nachdem der Program.cs
-Code der App ausgeführt wurde. Um eine andere Datenbank für die Tests zu verwenden, muss der Datenbankkontext der App in builder.ConfigureServices
ersetzt werden. Weitere Informationen finden Sie im Abschnitt Anpassen von WebApplicationFactory.