Integrationstests in ASP.NET Core

Von Javier Calvarro Nelson, Steve Smith und Jos van der Til

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.

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 es sich, ein Tool wie Playwright für .NET zu verwenden, 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“ bezeichnet. In diesem Thema wird „GS“ verwendet, um auf die getestete ASP.NET Core-App zu verweisen.

Tipp

Schreiben Sie keine Integrationstests für jede denkbare 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 führt die Verwendung von Infrastruktur-Fakes/-Pseudoobjekten zu einer schnelleren 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:

  1. Der Webhost des GS wird konfiguriert.
  2. Es wird ein Testserverclient erstellt, um Anforderungen an die App zu senden.
  3. Der Testschritt Arrange wird ausgeführt: Die Test-App bereitet eine Anforderung vor.
  4. Der Testschritt Act wird ausgeführt: Der Client sendet die Anforderung und empfängt die Antwort.
  5. Der Testschritt Assert wird ausgeführt: Die tatsächliche Antwort wird je nach der erwarteten Antwort als Pass oder Fail bewertet.
  6. Der Prozess wird so lange fortgesetzt, bis alle Tests ausgeführt wurden.
  7. 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 das bin 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.

Hinweis

Wenn Sie ein Testprojekt für eine App erstellen, verwenden Sie unterschiedliche Projekte für die Komponententests und die Integrationstests. Dadurch wird sichergestellt, dass Komponenten für Infrastrukturtests nicht versehentlich in die Komponententests eingeschlossen werden. Durch die Trennung von Komponenten- und Integrationstests können Sie außerdem 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:

  • Es muss auf das Paket Microsoft.AspNetCore.Mvc.Testing verwiesen werden.
  • 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:

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. Markieren Sie die Cookies in den Tests als unverzichtbar, um nicht erforderliche cookies beizubehalten, wie z. B. diejenigen, die vom TempData-Anbieter verwendet werden. 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:

  1. 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. Der builder.ConfigureServices-Rückruf der Test-App wird ausgeführt, nachdem der Startup.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 in builder.ConfigureServices ersetzt werden.

    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.

    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:

    services.AddDbContext<ApplicationDbContext>((options, context) => 
    {
        context.UseSqlServer(
            Configuration.GetConnectionString("TestingDbConnectionString"));
    });
    
  2. Verwenden Sie die benutzerdefinierte CustomWebApplicationFactory in Testklassen. Das folgende Beispiel verwendet die Factory in der IndexPageTests-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 einem Location-Header ist die erste Antwort eine Umleitung.

  3. 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:

  1. Senden einer Anforderung für die Seite.
  2. Analysieren des Fälschungsschutzcookies und des Anforderungsvalidierungstokens von der Antwort.
  3. 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 gibt IHtmlDocument zurück GetDocumentAsync verwendet eine Factory, die eine virtuelle Antwort basierend auf der ursprünglichen HttpResponseMessage vorbereitet. Weitere Informationen finden Sie in der AngleSharp-Dokumentation.
  • SendAsync-Erweiterungsmethoden für HttpClient erstellen eine HttpRequestMessage-Klasse und rufen SendAsync(HttpRequestMessage) auf, um Anforderungen an das GS zu übermitteln. Überladungen für SendAsync 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>>)

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.

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&#x27;ve an appointment in 
    London, and we&#x27;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&#x27;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 mit http://localhost/Identity/Account/Login beginnt. (Es wird nicht die abschließende Antwort der Anmeldeseite verwendet, bei der der Location-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 wie Testing).
  • Überschreiben Sie CreateHostBuilder in der Test-App, um Umgebungsvariablen mit dem Präfix ASPNETCORE 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 und Pages/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) und Text (Nachricht). Die Text-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:
  • Zugreifen auf eine sichere Seite durch einen nicht authentifizierten Benutzer.
  • Zugreifen auf eine sichere Seite durch einen authentifizierten Benutzer mit einem Pseudo-AuthenticationHandler<TOptions>.
  • Abrufen eines GitHub-Benutzerprofils und Überprüfen der Benutzeranmeldung des Profils.
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
  • Utilities.cs enthält die InitializeDbForTests-Methode, mit der ein Seeding der Datenbank mit Testdaten durchgeführt wird.
  • HtmlHelpers.cs stellt eine Methode bereit, mit der ein AngleSharp-IHtmlDocument zur Verwendung durch die Testmethoden zurückgegeben wird.
  • HttpClientExtensions.cs stellt Überladungen für SendAsync bereit, um Anforderungen an das GS zu senden.

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