Delen via


Integratietests in ASP.NET Core

Door Jos van der Til, Martin Costello en Javier Calvarro Nelson.

Integratietests zorgen ervoor dat de onderdelen van een app correct functioneren op een niveau dat de ondersteunende infrastructuur van de app bevat, zoals de database, het bestandssysteem en het netwerk. ASP.NET Core ondersteunt integratietests met behulp van een eenheidstestframework met een testhost en een in-memory testserver.

In dit artikel wordt ervan uitgegaan dat u basiskennis hebt van eenheidstests. Als u niet bekend bent met testconcepten, raadpleegt u het artikel Testen in .NET en de gekoppelde inhoud.

Voorbeeldcode bekijken of downloaden (hoe download je)

De voorbeeld-app is een Razor Pagina-app en gaat uit van basiskennis van Razor Pages. Als u niet bekend bent met Razor Pagina's, raadpleegt u de volgende artikelen:

Voor het testen van SPA's raden we een hulpprogramma aan, zoals Playwright voor .NET, waarmee een browser kan worden geautomatiseerd.

Inleiding tot integratietests

Integratietests evalueren de onderdelen van een app op een breder niveau dan eenheidstests. Eenheidstests worden gebruikt voor het testen van geïsoleerde softwareonderdelen, zoals afzonderlijke klassemethoden. Integratietests bevestigen dat twee of meer app-onderdelen samenwerken om een verwacht resultaat te produceren, mogelijk inclusief elk onderdeel dat nodig is om een aanvraag volledig te verwerken.

Deze bredere tests worden gebruikt om de infrastructuur en het hele framework van de app te testen, vaak inclusief de volgende onderdelen:

  • Gegevensbank
  • Bestandssysteem
  • Netwerkapparaten
  • Pijplijn voor aanvraag-antwoord

Eenheidstests maken gebruik van ge fabriceerde onderdelen, ook wel nep- of mockobjecten genoemd, in plaats van infrastructuuronderdelen.

In tegenstelling tot eenheidstests, integratietests:

  • Gebruik de werkelijke onderdelen die door de app in productie worden gebruikt.
  • Meer code en gegevensverwerking vereisen.
  • Het duurt langer om uit te voeren.

Beperk daarom het gebruik van integratietests tot de belangrijkste infrastructuurscenario's. Als een gedrag kan worden getest met behulp van een eenheidstest of een integratietest, kiest u de eenheidstest.

In discussies over integratietests wordt het geteste project kortom het systeem onder test of 'SUT' genoemd. "SUT" wordt in dit artikel gebruikt om te verwijzen naar de ASP.NET Core-app die wordt getest.

Schrijf geen integratietests voor elke permutatie van gegevens- en bestandstoegang met databases en bestandssystemen. Ongeacht het aantal plaatsen in een app dat communiceert met databases en bestandssystemen, is een gerichte set integratietests voor lezen, schrijven, bijwerken en verwijderen meestal geschikt voor het testen van database- en bestandssysteemonderdelen. Gebruik eenheidstests voor routinetests van methodelogica die met deze onderdelen communiceren. In eenheidstests resulteert het gebruik van infrastructuurvervalsingen of mocks in een snellere testuitvoering.

ASP.NET Kernintegratietests

Integratietests in ASP.NET Core vereisen het volgende:

  • Een testproject wordt gebruikt om de tests te bevatten en uit te voeren. Het testproject heeft een verwijzing naar de SUT.
  • Het testproject maakt een testwebhost voor de SUT en gebruikt een testserverclient voor het afhandelen van aanvragen en antwoorden met de SUT.
  • Een testloper wordt gebruikt om de tests uit te voeren en de testresultaten te rapporteren.

Integratietests volgen een reeks gebeurtenissen die de gebruikelijke teststappen Rangschikken, Act en Assert bevatten:

  1. De webhost van de SUT is geconfigureerd.
  2. Er wordt een testserverclient gemaakt om aanvragen naar de app te verzenden.
  3. De teststap Rangschikken wordt uitgevoerd: De test-app bereidt een aanvraag voor.
  4. De teststap Act wordt uitgevoerd: de client verzendt de aanvraag en ontvangt het antwoord.
  5. De assert-teststap wordt uitgevoerd: het daadwerkelijke antwoord wordt gevalideerd als een geslaagd of mislukt op basis van een verwacht antwoord.
  6. Het proces wordt voortgezet totdat alle tests worden uitgevoerd.
  7. De testresultaten worden gerapporteerd.

Normaal gesproken is de testwebhost anders geconfigureerd dan de normale webhost van de app voor de testuitvoeringen. Een andere database of andere app-instellingen kunnen bijvoorbeeld worden gebruikt voor de tests.

Infrastructuuronderdelen, zoals de testwebhost en de testserver in het geheugen (TestServer), worden geleverd of beheerd door het microsoft.AspNetCore.Mvc.Testing-pakket . Het gebruik van dit pakket stroomlijnt het maken en uitvoeren van tests.

Het Microsoft.AspNetCore.Mvc.Testing pakket verwerkt de volgende taken:

  • Kopieert het afhankelijkhedenbestand (.deps) van de SUT naar de map van bin het testproject.
  • Hiermee stelt u de inhoudshoofdmap in op de hoofdmap van het SUT-project, zodat statische bestanden en pagina's/weergaven worden gevonden wanneer de tests worden uitgevoerd.
  • Biedt de klasse WebApplicationFactory om het bootstrappen van de SUT te stroomlijnen.TestServer

In de documentatie voor eenheidstests wordt beschreven hoe u een testproject en testloper instelt, samen met gedetailleerde instructies voor het uitvoeren van tests en aanbevelingen voor het benoemen van tests en testklassen.

Afzonderlijke eenheidstests van integratietests in verschillende projecten. Het scheiden van de tests:

  • Helpt ervoor te zorgen dat onderdelen voor infrastructuurtests niet per ongeluk worden opgenomen in de eenheidstests.
  • Hiermee kunt u bepalen welke set tests worden uitgevoerd.

Er is vrijwel geen verschil tussen de configuratie voor tests van Razor Pagina-apps en MVC-apps. Het enige verschil is in de naam van de tests. In een Razor Pagina-app worden tests van pagina-eindpunten meestal vernoemd naar de paginamodelklasse (bijvoorbeeld IndexPageTests om de integratie van onderdelen voor de indexpagina te testen). In een MVC-app worden tests meestal ingedeeld op controllerklassen en benoemd naar de controllers die ze testen (bijvoorbeeld HomeControllerTests om de integratie van onderdelen voor de Home controller te testen).

Vereisten voor de app testen

Het testproject moet:

Deze vereisten kunnen worden weergegeven in de voorbeeld-app. Inspecteer het tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj bestand. De voorbeeld-app maakt gebruik van het xUnit-testframework en de AngleSharp-parserbibliotheek , dus de voorbeeld-app verwijst ook naar:

In apps die versie 2.4.2 of hoger gebruiken xunit.runner.visualstudio , moet het testproject verwijzen naar het Microsoft.NET.Test.Sdk pakket.

Entity Framework Core wordt ook gebruikt in de tests. Bekijk het projectbestand in GitHub.

SUT-omgeving

Als de omgeving van de SUT niet is ingesteld, wordt de omgeving standaard ingesteld op Ontwikkeling.

Basistests met de standaard WebApplicationFactory

WebApplicationFactory<TEntryPoint> wordt gebruikt om een TestServer voor de integratietests te maken. TEntryPoint is de ingangspuntklasse van de SUT, meestal Program.cs.

Testklassen implementeren een interface voor klassearmaturen (IClassFixture) om aan te geven dat de klasse tests bevat en gedeelde objectexemplaren biedt voor de tests in de klasse.

De volgende testklasse, BasicTests, gebruikt de WebApplicationFactory om de SUT te bootstrapen en verstrekt een HttpClient aan een testmethode, Get_EndpointsReturnSuccessAndCorrectContentType. De methode controleert of de antwoordstatuscode succesvol is (200-299) en of de Content-Type header text/html; charset=utf-8 is voor verschillende app-pagina's.

CreateClient() maakt een instantie van HttpClient dat automatisch omleidingen volgt en cookies verwerkt.

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());
    }
}
[TestClass]
public class BasicTests
{
    private static CustomWebApplicationFactory<Program> _factory;

    [ClassInitialize]
    public static void AssemblyInitialize(TestContext _)
    {
        _factory = new CustomWebApplicationFactory<Program>();
    }

    [ClassCleanup(ClassCleanupBehavior.EndOfClass)]
    public static void AssemblyCleanup(TestContext _)
    {
        _factory.Dispose();
    }

    [TestMethod]
    [DataRow("/")]
    [DataRow("/Index")]
    [DataRow("/About")]
    [DataRow("/Privacy")]
    [DataRow("/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.AreEqual("text/html; charset=utf-8",
            response.Content.Headers.ContentType.ToString());
    }
}
public class BasicTests
{
    private CustomWebApplicationFactory<Program>
        _factory;

    [SetUp]
    public void SetUp()
    {
        _factory = new CustomWebApplicationFactory<Program>();
    }

    [TearDown]
    public void TearDown()
    {
        _factory.Dispose();
    }

    [DatapointSource]
    public string[] values = ["/", "/Index", "/About", "/Privacy", "/Contact"];

    [Theory]
    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.That(response.Content.Headers.ContentType.ToString(), Is.EqualTo("text/html; charset=utf-8"));
    }
}

Standaard worden niet-essentiële cookies niet bewaard voor aanvragen wanneer het toestemmingsbeleid voor algemene gegevensbescherming is ingeschakeld. Als u niet-essentiële cookies wilt behouden, zoals cookies die door de TempData-provider worden gebruikt, markeert u deze als essentieel in uw tests. Zie cookie voor instructies over het markeren van een als essentieel onderdeel.

AngleSharp versus Application Parts voor antiforgery-controlesystemen

In dit artikel wordt de AngleSharp-parser gebruikt om de antivervalsingcontroles af te handelen door pagina's te laden en de HTML te parseren. Voor het testen van de eindpunten van controller- en Razor paginaweergaven op een lager niveau, zonder te zorgen over hoe ze in de browser worden weergegeven, kunt u overwegen om te gebruiken Application Parts. De benadering toepassingsonderdelen injecteert een controller of Razor pagina in de app die kan worden gebruikt om JSON-aanvragen te maken om de vereiste waarden op te halen. Zie de blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts and associated GitHub repo by Martin Costello (Engelstalig) voor meer informatie.

WebApplicationFactory aanpassen

De configuratie van de webhost kan onafhankelijk van de testklassen worden gemaakt door te erven van WebApplicationFactory<TEntryPoint> om een of meer aangepaste fabrieken te creëren.

  1. Overnemen van WebApplicationFactory en overschrijven ConfigureWebHost. Met IWebHostBuilder kan de serviceverzameling worden geconfigureerd met IWebHostBuilder.ConfigureServices

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType == 
                        typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    
    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    
    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    

    Database-seeding in de voorbeeld-app wordt uitgevoerd door de InitializeDbForTests methode. De methode wordt beschreven in het voorbeeld van integratietests: sectie App-organisatie testen .

    De databasecontext van de SUT is geregistreerd in Program.cs. De callback van de test-app builder.ConfigureServices wordt uitgevoerd nadat de code van de app Program.cs is uitgevoerd. Als u een andere database wilt gebruiken voor de tests dan de database van de app, moet de databasecontext van de app worden vervangen in builder.ConfigureServices.

    De voorbeeld-app vindt de servicedescriptor voor de databasecontext en gebruikt de descriptor om de serviceregistratie te verwijderen. De factory voegt vervolgens een nieuwe ApplicationDbContext toe die gebruikmaakt van een in-memory database voor de tests.

    Als u verbinding wilt maken met een andere database, wijzigt u de DbConnection. Een SQL Server-testdatabase gebruiken:

  1. Gebruik de aangepaste CustomWebApplicationFactory in testklassen. In het volgende voorbeeld wordt de factory in de IndexPageTests klasse gebruikt:

    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
            });
        }
    
    [TestClass]
    public class IndexPageTests
    {
        private static HttpClient _client;
        private static CustomWebApplicationFactory<Program>
            _factory;
    
        [ClassInitialize]
        public static void AssemblyInitialize(TestContext _)
        {
            _factory = new CustomWebApplicationFactory<Program>();
            _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
        }
    
        [ClassCleanup(ClassCleanupBehavior.EndOfClass)]
        public static void AssemblyCleanup(TestContext _)
        {
            _factory.Dispose();
        }
    
    public class IndexPageTests
    {
    
        private HttpClient _client;
        private CustomWebApplicationFactory<Program>
            _factory;
    
        [SetUp]
        public void SetUp()
        {
            _factory = new CustomWebApplicationFactory<Program>();
            _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
        }
    
        [TearDown]
        public void TearDown()
        {
            _factory.Dispose();
            _client.Dispose();
        }
    

    De client van de voorbeeld-app is zo geconfigureerd dat het HttpClient geen omleidingen volgt. Zoals verderop in de sectie Mock-verificatie wordt uitgelegd, kunnen tests het resultaat van het eerste antwoord van de app controleren. De eerste reactie is een doorverwijzing in veel van deze tests met een Location header.

  2. Een typische test maakt gebruik van de HttpClient en helpermethoden om de aanvraag en het antwoord te verwerken:

    [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);
    }
    
    [TestMethod]
    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.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
        Assert.AreEqual("/", response.Headers.Location.OriginalString);
    }
    
    [Test]
    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.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK));
        Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
        Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/"));
    }
    

Elke POST-aanvraag voor de SUT moet voldoen aan de antivervalsingscontrole die automatisch wordt uitgevoerd door het antivervalsingssysteem voor gegevensbeveiliging van de app. Als u de POST-aanvraag van een test wilt regelen, moet de test-app het volgende doen:

  1. Maak een aanvraag voor de pagina.
  2. Parse het antiforgery-token en het aanvraagvalidatietoken uit cookie het antwoord.
  3. Voer de POST-aanvraag uit met de antiforgery- en aanvraagvalidatietokens cookie.

De SendAsync helper-extensiemethoden () en de Helpers/HttpClientExtensions.cs helpermethode (GetDocumentAsyncHelpers/HtmlHelpers.cs) in de voorbeeld-app gebruiken de AngleSharp-parser om de antivervalsingscontrole af te handelen met de volgende methoden:

  • GetDocumentAsync: ontvangt de HttpResponseMessage en retourneert een IHtmlDocument. GetDocumentAsync maakt gebruik van een fabriek die een virtueel antwoord voorbereidt op basis van het oorspronkelijke HttpResponseMessageantwoord. Zie de documentatie van AngleSharp voor meer informatie.
  • SendAsync extensiemethoden voor het HttpClient opstellen van een HttpRequestMessage en aanroep SendAsync(HttpRequestMessage) om aanvragen naar de SUT te verzenden. Overbelastingen voor SendAsync het accepteren van het HTML-formulier (IHtmlFormElement) en het volgende:
    • Knop Verzenden van het formulier (IHtmlElement)
    • Formulierwaardenverzameling (IEnumerable<KeyValuePair<string, string>>)
    • Verzendknop (IHtmlElement) en formulierwaarden (IEnumerable<KeyValuePair<string, string>>)

AngleSharp is een parseringsbibliotheek van derden die wordt gebruikt voor demonstratiedoeleinden in dit artikel en de voorbeeld-app. AngleSharp wordt niet ondersteund of vereist voor integratietests van ASP.NET Core-apps. Andere parsers kunnen worden gebruikt, zoals het HTML Agility Pack (HAP). Een andere methode is het schrijven van code voor de directe verwerking van het verificatietoken voor aanvragen van het antivervalsingssysteem en cookie. Zie in dit artikel AngleSharp versus Application Parts antiforgery-controles voor meer informatie.

De EF-Core in-memory databaseprovider kan worden gebruikt voor beperkte en eenvoudige tests, maar de SQLite-provider is de aanbevolen keuze voor in-memory tests.

Zie Opstarten uitbreiden met opstartfilters die laten zien hoe u middleware configureert met behulp van IStartupFilter, wat handig is wanneer een test een aangepaste service of middleware vereist.

De client aanpassen met WithWebHostBuilder

Wanneer er extra configuratie is vereist binnen een testmethode, creëert WithWebHostBuilder een nieuwe WebApplicationFactory met een IWebHostBuilder die verder wordt aangepast door middel van configuratie.

De voorbeeldcode roept WithWebHostBuilder om ingerichte services te vervangen door test-stubs. Zie Mock-services injecteren in dit artikel voor meer informatie en voorbeeldgebruik.

De Post_DeleteMessageHandler_ReturnsRedirectToRoot testmethode van de voorbeeld-app demonstreert het gebruik van WithWebHostBuilder. Met deze test wordt een record verwijderd in de database door een formulierverzending in de SUT te activeren.

Omdat een andere test in de IndexPageTests klasse een bewerking uitvoert waarmee alle records in de database worden verwijderd en die vóór de Post_DeleteMessageHandler_ReturnsRedirectToRoot methode kunnen worden uitgevoerd, wordt de database opnieuw verzonden in deze testmethode om ervoor te zorgen dat er een record aanwezig is om de SUT te verwijderen. Het selecteren van de eerste verwijderknop van het messages formulier in de SUT wordt gesimuleerd in de aanvraag naar de SUT:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}
[TestMethod]
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.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
    Assert.AreEqual("/", response.Headers.Location.OriginalString);
}
[Test]
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.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK));
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
    Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/"));
}

Clientopties

Zie de WebApplicationFactoryClientOptions pagina voor standaardinstellingen en beschikbare opties bij het maken van HttpClient exemplaren.

Maak de WebApplicationFactoryClientOptions klasse en geef deze door aan de 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
        });
    }
[TestClass]
public class IndexPageTests
{
    private static HttpClient _client;
    private static CustomWebApplicationFactory<Program>
        _factory;

    [ClassInitialize]
    public static void AssemblyInitialize(TestContext _)
    {
        _factory = new CustomWebApplicationFactory<Program>();
        _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

    [ClassCleanup(ClassCleanupBehavior.EndOfClass)]
    public static void AssemblyCleanup(TestContext _)
    {
        _factory.Dispose();
    }
public class IndexPageTests
{

    private HttpClient _client;
    private CustomWebApplicationFactory<Program>
        _factory;

    [SetUp]
    public void SetUp()
    {
        _factory = new CustomWebApplicationFactory<Program>();
        _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

    [TearDown]
    public void TearDown()
    {
        _factory.Dispose();
        _client.Dispose();
    }

NOTITIE: Als u HTTPS-omleidingswaarschuwingen in logboeken wilt voorkomen bij het gebruik van HTTPS Redirection Middleware, stelt u BaseAddress = new Uri("https://localhost")

Mock-services injecteren

Services kunnen in een test worden overschreven met een aanroep naar ConfigureTestServices op de host builder. Als u het bereik van de overschreven services voor de test zelf wilt instellen, wordt de WithWebHostBuilder methode gebruikt om een hostbouwer op te halen. Dit is te zien in de volgende tests:

De voorbeeld-SUT bevat een scoped service die een offerte retourneert. Het citaat wordt ingesloten in een verborgen veld op de indexpagina wanneer de indexpagina wordt aangevraagd.

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

De volgende markeringen worden gegenereerd wanneer de SUT-app wordt uitgevoerd:

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

Om de service en offerteinjectie in een integratietest te testen, wordt door de test een mockservice in de SUT geïnjecteerd. De mockservice vervangt de QuoteService van de app door een service die wordt geleverd door de test-app, genaamd TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}
// 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.");
    }
}
// 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 wordt aangeroepen en de scoped service wordt geregistreerd:

[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);
}
[TestMethod]
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.AreEqual("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}
[Test]
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.That(quoteElement.Attributes["value"].Value, Is.EqualTo(
        "Something's interfering with time, Mr. Scarman, " +
        "and time is my business."));
}

De opmaak die tijdens de uitvoering van de test wordt geproduceerd, weerspiegelt de citaattekst die is opgegeven door TestQuoteService, waardoor de assertie slaagt.

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

Simulatie-authenticatie

Tests in de AuthTests-klasse controleren of een beveiligd eindpunt:

  • Hiermee wordt een niet-geverifieerde gebruiker omgeleid naar de aanmeldingspagina van de app.
  • Retourneert inhoud voor een geverifieerde gebruiker.

In de SUT gebruikt de /SecurePage pagina een AuthorizePage conventie om een AuthorizeFilter op de pagina toe te passen. Zie Razor Autorisatieconventies voor pagina's voor meer informatie.

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

In de Get_SecurePageRedirectsAnUnauthenticatedUser test wordt een WebApplicationFactoryClientOptions ingesteld om omleidingen te verbieden door AllowAutoRedirect op false in te stellen.

[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);
}
[TestMethod]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
    StringAssert.StartsWith(response.Headers.Location.OriginalString, "http://localhost/Identity/Account/Login");
}
[Test]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
    Assert.That(response.Headers.Location.OriginalString, Does.StartWith("http://localhost/Identity/Account/Login"));
}

Door te voorkomen dat de client de omleiding volgt, kunnen de volgende controles worden uitgevoerd:

  • De statuscode die door de SUT wordt geretourneerd, kan worden gecontroleerd op basis van het verwachte HttpStatusCode.Redirect resultaat, niet de uiteindelijke statuscode na de omleiding naar de aanmeldingspagina, wat zou zijn HttpStatusCode.OK.
  • De Location headerwaarde in de antwoordheaders wordt gecontroleerd om te bevestigen dat deze begint met http://localhost/Identity/Account/Login, niet het laatste antwoord op de aanmeldingspagina, waar de Location header niet aanwezig zou zijn.

De testapp kan een AuthenticationHandler<TOptions> in ConfigureTestServices simuleren om aspecten van authenticatie en autorisatie te testen. Een minimaal scenario retourneert een AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
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);
    }
}
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);
    }
}

De TestAuthHandler wordt aangeroepen om een gebruiker te authenticeren wanneer het verificatieschema is ingesteld op TestScheme waar AddAuthentication geregistreerd is voor ConfigureTestServices. Het is belangrijk dat het TestScheme schema overeenkomt met het schema dat uw app verwacht. Anders werkt verificatie niet.

[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);
}
[TestMethod]
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.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
[Test]
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.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}

Zie de sectie WebApplicationFactoryClientOptions voor meer informatie.

Basistests voor verificatie-middleware

Bekijk deze GitHub-opslagplaats voor basistests van verificatie-middleware. Het bevat een testserver die specifiek is voor het testscenario.

De omgeving instellen

Stel de omgeving in de aangepaste applicatiefabriek in.

public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType == 
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}

Hoe de testinfrastructuur het app-inhoudsbasispad afleidt

De WebApplicationFactory constructor bepaalt het hoofdpad van de app-inhoud door te zoeken naar een WebApplicationFactoryContentRootAttribute op de assembly met de integratietests met een sleutel die gelijk is aan de TEntryPoint assembly System.Reflection.Assembly.FullName. Als een kenmerk met de juiste sleutel niet wordt gevonden, wordt overgeschakeld naar het zoeken naar een solutionbestand (WebApplicationFactory) en wordt de assemblynaam toegevoegd aan de solution directory. De hoofdmap van de app (het hoofdpad van de inhoud) wordt gebruikt om weergaven en inhoudsbestanden te detecteren.

Schaduwkopie uitschakelen

Schaduwkopie zorgt ervoor dat de tests worden uitgevoerd in een andere map dan de uitvoermap. Als uw tests afhankelijk zijn van het laden van bestanden ten opzichte Assembly.Location van en u problemen ondervindt, moet u mogelijk schaduwkopie uitschakelen.

Als u schaduwkopie wilt uitschakelen wanneer u xUnit gebruikt, maakt u een xunit.runner.json bestand in uw testprojectmap met de juiste configuratie-instelling:

{
  "shadowCopy": false
}

Verwijdering van objecten

Nadat de tests van de IClassFixture-implementatie zijn uitgevoerd, worden TestServer en HttpClient verwijderd wanneer xUnit de WebApplicationFactory verwijdert. Als objecten die door de ontwikkelaar zijn geïnstantieerd, verwijdering vereisen, moet u deze verwijderen in de IClassFixture implementatie. Zie Een verwijderingsmethode implementeren voor meer informatie.

Nadat de tests van TestClass zijn uitgevoerd, worden TestServer en HttpClient verwijderd wanneer MSTest de WebApplicationFactory in de ClassCleanup-methode verwijdert. Als objecten die door de ontwikkelaar zijn geïnstantieerd, verwijdering vereisen, moet u deze in de ClassCleanup methode verwijderen. Zie Een verwijderingsmethode implementeren voor meer informatie.

Nadat de tests van de testklasse zijn uitgevoerd, worden TestServer en HttpClient verwijderd wanneer NUnit WebApplicationFactory afhandelt tijdens de TearDown methode. Als objecten die door de ontwikkelaar zijn geïnstantieerd, verwijdering vereisen, moet u deze in de TearDown methode verwijderen. Zie Een verwijderingsmethode implementeren voor meer informatie.

Voorbeeld van integratietests

De voorbeeld-app bestaat uit twee apps:

Applicatie Projectmap Beschrijving
Bericht-app (de SUT) src/RazorPagesProject Hiermee kan een gebruiker berichten toevoegen, verwijderen, alles verwijderen en analyseren.
App testen tests/RazorPagesProject.Tests Wordt gebruikt om de integratietest van de SUT uit te voeren.

De tests kunnen worden uitgevoerd met behulp van de ingebouwde testfuncties van een IDE, zoals Visual Studio. Als u Visual Studio Code of de opdrachtregel gebruikt, voert u de volgende opdracht uit bij een opdrachtprompt in de tests/RazorPagesProject.Tests map:

dotnet test

Organisatie van de berichtenapp (SUT)

De SUT is een Razor paginaberichtsysteem met de volgende kenmerken:

  • De indexpagina van de app (Pages/Index.cshtml en Pages/Index.cshtml.cs) biedt een ui- en paginamodelmethode om de toevoeging, verwijdering en analyse van berichten (gemiddelde woorden per bericht) te beheren.
  • Een bericht wordt beschreven door de Message klasse (Data/Message.cs) met twee eigenschappen: Id (sleutel) en Text (bericht). De Text eigenschap is vereist en beperkt tot 200 tekens.
  • Berichten worden opgeslagen met behulp van de in-memory database van Entity Framework†.
  • De app bevat een DATA Access Layer (DAL) in de databasecontextklasse (AppDbContextData/AppDbContext.cs).
  • Als de database leeg is bij het opstarten van de app, wordt het berichtenarchief geïnitialiseerd met drie berichten.
  • De app bevat een /SecurePage die alleen toegankelijk is voor een geauthenticeerde gebruiker.

†De EF-artikel, Testen met InMemory, legt uit hoe u een in-memory database gebruikt voor tests met MSTest. In dit onderwerp wordt het xUnit-testframework gebruikt. Testconcepten en test-implementaties in verschillende testframeworks zijn vergelijkbaar, maar niet identiek.

Hoewel de app geen gebruik maakt van het opslagplaatspatroon en geen effectief voorbeeld is van het UoW-patroon (Unit of Work),Razor ondersteunt Pages deze patronen van ontwikkeling. Zie Voor meer informatie het ontwerpen van de infrastructuurpersistentielaag en testcontrollerlogica (het voorbeeld implementeert het opslagplaatspatroon).

App-organisatie testen

De test-app is een console-app in de tests/RazorPagesProject.Tests map.

App-map testen Beschrijving
AuthTests Bevat testmethoden voor:
  • Toegang tot een beveiligde pagina door een niet-geverifieerde gebruiker.
  • Toegang tot een beveiligde pagina door een geverifieerde gebruiker met een mock AuthenticationHandler<TOptions>.
  • Het verkrijgen van een GitHub-gebruikersprofiel en het controleren van de gebruikersaanmelding van het profiel.
BasicTests Bevat een testmethode voor routering en inhoudstype.
IntegrationTests Bevat de integratietests voor de pagina Index met behulp van aangepaste WebApplicationFactory klasse.
Helpers/Utilities
  • Utilities.cs bevat de InitializeDbForTests methode die wordt gebruikt om de database te seeden met testgegevens.
  • HtmlHelpers.cs biedt een methode voor het retourneren van een AngleSharp IHtmlDocument voor gebruik in de testmethoden.
  • HttpClientExtensions.cs biedt overbelastingen voor SendAsync het indienen van aanvragen bij de SUT.

Het testframework is xUnit. Integratietests worden uitgevoerd met de Microsoft.AspNetCore.TestHost, die de TestServer omvat. Omdat het Microsoft.AspNetCore.Mvc.Testing pakket wordt gebruikt voor het configureren van de testhost en testserver, vereisen de pakketten TestHost en TestServer geen directe pakketverwijzingen in het projectbestand van de test-app of in de configuratie voor de ontwikkelaar.

Integratietests vereisen meestal een kleine gegevensset in de database voordat de test wordt uitgevoerd. Met een testoproep voor verwijderen van een databaserecord moet de database bijvoorbeeld ten minste één record hebben om de verwijderaanvraag te laten slagen.

De voorbeeld-app vult de database met drie berichten in Utilities.cs die tests kunnen gebruiken wanneer ze worden uitgevoerd.

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

De databasecontext van de SUT is geregistreerd in Program.cs. De callback van de test-app builder.ConfigureServices wordt uitgevoerd nadat de code van de app Program.cs is uitgevoerd. Als u een andere database voor de tests wilt gebruiken, moet de databasecontext van de app worden vervangen in builder.ConfigureServices. Zie de sectie WebApplicationFactory aanpassen voor meer informatie.

Aanvullende bronnen

In dit onderwerp wordt ervan uitgegaan dat u basiskennis hebt van eenheidstests. Als u niet bekend bent met testconcepten, raadpleegt u het onderwerp Unit Testing in .NET Core en .NET Standard en de gekoppelde inhoud.

Voorbeeldcode bekijken of downloaden (hoe download je)

De voorbeeld-app is een Razor Pagina-app en gaat uit van basiskennis van Razor Pages. Als u niet bekend bent met Razor Pagina's, raadpleegt u de volgende onderwerpen:

Opmerking

Voor het testen van SPA's raden we een hulpprogramma aan, zoals Playwright voor .NET, waarmee een browser kan worden geautomatiseerd.

Inleiding tot integratietests

Integratietests evalueren de onderdelen van een app op een breder niveau dan eenheidstests. Eenheidstests worden gebruikt voor het testen van geïsoleerde softwareonderdelen, zoals afzonderlijke klassemethoden. Integratietests bevestigen dat twee of meer app-onderdelen samenwerken om een verwacht resultaat te produceren, mogelijk inclusief elk onderdeel dat nodig is om een aanvraag volledig te verwerken.

Deze bredere tests worden gebruikt om de infrastructuur en het hele framework van de app te testen, vaak inclusief de volgende onderdelen:

  • Gegevensbank
  • Bestandssysteem
  • Netwerkapparaten
  • Pijplijn voor aanvraag-antwoord

Eenheidstests maken gebruik van ge fabriceerde onderdelen, ook wel nep- of mockobjecten genoemd, in plaats van infrastructuuronderdelen.

In tegenstelling tot eenheidstests, integratietests:

  • Gebruik de werkelijke onderdelen die door de app in productie worden gebruikt.
  • Meer code en gegevensverwerking vereisen.
  • Het duurt langer om uit te voeren.

Beperk daarom het gebruik van integratietests tot de belangrijkste infrastructuurscenario's. Als een gedrag kan worden getest met behulp van een eenheidstest of een integratietest, kiest u de eenheidstest.

In discussies over integratietests wordt het geteste project kortom het systeem onder test of 'SUT' genoemd. "SUT" wordt in dit artikel gebruikt om te verwijzen naar de ASP.NET Core-app die wordt getest.

Schrijf geen integratietests voor elke permutatie van gegevens- en bestandstoegang met databases en bestandssystemen. Ongeacht het aantal plaatsen in een app dat communiceert met databases en bestandssystemen, is een gerichte set integratietests voor lezen, schrijven, bijwerken en verwijderen meestal geschikt voor het testen van database- en bestandssysteemonderdelen. Gebruik eenheidstests voor routinetests van methodelogica die met deze onderdelen communiceren. In eenheidstests resulteert het gebruik van infrastructuurvervalsingen of mocks in een snellere testuitvoering.

ASP.NET Kernintegratietests

Integratietests in ASP.NET Core vereisen het volgende:

  • Een testproject wordt gebruikt om de tests te bevatten en uit te voeren. Het testproject heeft een verwijzing naar de SUT.
  • Het testproject maakt een testwebhost voor de SUT en gebruikt een testserverclient voor het afhandelen van aanvragen en antwoorden met de SUT.
  • Een testloper wordt gebruikt om de tests uit te voeren en de testresultaten te rapporteren.

Integratietests volgen een reeks gebeurtenissen die de gebruikelijke teststappen Rangschikken, Act en Assert bevatten:

  1. De webhost van de SUT is geconfigureerd.
  2. Er wordt een testserverclient gemaakt om aanvragen naar de app te verzenden.
  3. De teststap Rangschikken wordt uitgevoerd: De test-app bereidt een aanvraag voor.
  4. De teststap Act wordt uitgevoerd: de client verzendt de aanvraag en ontvangt het antwoord.
  5. De assert-teststap wordt uitgevoerd: het daadwerkelijke antwoord wordt gevalideerd als een geslaagd of mislukt op basis van een verwacht antwoord.
  6. Het proces wordt voortgezet totdat alle tests worden uitgevoerd.
  7. De testresultaten worden gerapporteerd.

Normaal gesproken is de testwebhost anders geconfigureerd dan de normale webhost van de app voor de testuitvoeringen. Een andere database of andere app-instellingen kunnen bijvoorbeeld worden gebruikt voor de tests.

Infrastructuuronderdelen, zoals de testwebhost en de testserver in het geheugen (TestServer), worden geleverd of beheerd door het microsoft.AspNetCore.Mvc.Testing-pakket . Het gebruik van dit pakket stroomlijnt het maken en uitvoeren van tests.

Het Microsoft.AspNetCore.Mvc.Testing pakket verwerkt de volgende taken:

  • Kopieert het afhankelijkhedenbestand (.deps) van de SUT naar de map van bin het testproject.
  • Hiermee stelt u de inhoudshoofdmap in op de hoofdmap van het SUT-project, zodat statische bestanden en pagina's/weergaven worden gevonden wanneer de tests worden uitgevoerd.
  • Biedt de klasse WebApplicationFactory om het bootstrappen van de SUT te stroomlijnen.TestServer

In de documentatie voor eenheidstests wordt beschreven hoe u een testproject en testloper instelt, samen met gedetailleerde instructies voor het uitvoeren van tests en aanbevelingen voor het benoemen van tests en testklassen.

Afzonderlijke eenheidstests van integratietests in verschillende projecten. Het scheiden van de tests:

  • Helpt ervoor te zorgen dat onderdelen voor infrastructuurtests niet per ongeluk worden opgenomen in de eenheidstests.
  • Hiermee kunt u bepalen welke set tests worden uitgevoerd.

Er is vrijwel geen verschil tussen de configuratie voor tests van Razor Pagina-apps en MVC-apps. Het enige verschil is in de naam van de tests. In een Razor Pagina-app worden tests van pagina-eindpunten meestal vernoemd naar de paginamodelklasse (bijvoorbeeld IndexPageTests om de integratie van onderdelen voor de indexpagina te testen). In een MVC-app worden tests meestal ingedeeld op controllerklassen en benoemd naar de controllers die ze testen (bijvoorbeeld HomeControllerTests om de integratie van onderdelen voor de Home controller te testen).

Vereisten voor de app testen

Het testproject moet:

Deze vereisten kunnen worden weergegeven in de voorbeeld-app. Inspecteer het tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj bestand. De voorbeeld-app maakt gebruik van het xUnit-testframework en de AngleSharp-parserbibliotheek , dus de voorbeeld-app verwijst ook naar:

In apps die versie 2.4.2 of hoger gebruiken xunit.runner.visualstudio , moet het testproject verwijzen naar het Microsoft.NET.Test.Sdk pakket.

Entity Framework Core wordt ook gebruikt in de tests. De app-verwijzingen:

SUT-omgeving

Als de omgeving van de SUT niet is ingesteld, wordt de omgeving standaard ingesteld op Ontwikkeling.

Basistests met de standaard WebApplicationFactory

WebApplicationFactory<TEntryPoint> wordt gebruikt om een TestServer voor de integratietests te maken. TEntryPoint is de ingangspuntklasse van de SUT, meestal de Startup klasse.

Testklassen implementeren een interface voor klassearmaturen (IClassFixture) om aan te geven dat de klasse tests bevat en gedeelde objectexemplaren biedt voor de tests in de klasse.

De volgende testklasse, BasicTests, gebruikt de WebApplicationFactory om de SUT te bootstrapen en verstrekt een HttpClient aan een testmethode, Get_EndpointsReturnSuccessAndCorrectContentType. De methode controleert of de antwoordstatuscode succesvol is (statuscodes van 200 tot en met 299) en dat de Content-Type header text/html; charset=utf-8 is voor verschillende apppagina's.

CreateClient() maakt een instantie van HttpClient dat automatisch omleidingen volgt en cookies verwerkt.

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

Standaard worden niet-essentiële cookies niet bewaard in aanvragen wanneer het AVG-toestemmingsbeleid is ingeschakeld. Als u niet-essentiële cookies wilt behouden, zoals cookies die door de TempData-provider worden gebruikt, markeert u deze als essentieel in uw tests. Zie cookie voor instructies over het markeren van een als essentieel onderdeel.

WebApplicationFactory aanpassen

De configuratie van de webhost kan onafhankelijk van de testklassen worden gemaakt door te erven van WebApplicationFactory om een of meer aangepaste fabrieken te creëren.

  1. Overnemen van WebApplicationFactory en overschrijven ConfigureWebHost. Met IWebHostBuilder kan de configuratie van de serviceverzameling worden bepaald met 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);
                    }
                }
            });
        }
    }
    

    Database-seeding in de voorbeeld-app wordt uitgevoerd door de InitializeDbForTests methode. De methode wordt beschreven in het voorbeeld van integratietests: sectie App-organisatie testen .

    De databasecontext van de SUT wordt geregistreerd in de Startup.ConfigureServices methode. De callback van de test-app builder.ConfigureServices wordt uitgevoerd nadat de code van de app Startup.ConfigureServices is uitgevoerd. De uitvoeringsvolgorde is een belangrijke wijziging voor de Generic Host met de release van ASP.NET Core 3.0. Als u een andere database wilt gebruiken voor de tests dan de database van de app, moet de databasecontext van de app worden vervangen in builder.ConfigureServices.

    Voor SUT's die nog steeds gebruikmaken van de webhost, wordt de callback van builder.ConfigureServices de test-app uitgevoerd vóór de code van Startup.ConfigureServices de SUT. De callback van builder.ConfigureTestServices de test-app wordt uitgevoerd.

    De voorbeeld-app vindt de servicedescriptor voor de databasecontext en gebruikt de descriptor om de serviceregistratie te verwijderen. Vervolgens voegt de factory een nieuwe ApplicationDbContext toe die gebruikmaakt van een in-memory database voor de tests.

    Als u verbinding wilt maken met een andere database dan de in-memory database, wijzigt u de UseInMemoryDatabase aanroep om de context te verbinden met een andere database. Een SQL Server-testdatabase gebruiken:

    services.AddDbContext<ApplicationDbContext>((options, context) => 
    {
        context.UseSqlServer(
            Configuration.GetConnectionString("TestingDbConnectionString"));
    });
    
  2. Gebruik de aangepaste CustomWebApplicationFactory in testklassen. In het volgende voorbeeld wordt de factory in de IndexPageTests klasse gebruikt:

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

    De client van de voorbeeld-app is zo geconfigureerd dat het HttpClient geen omleidingen volgt. Zoals verderop in de sectie Mock-verificatie wordt uitgelegd, kunnen tests het resultaat van het eerste antwoord van de app controleren. De eerste reactie is een doorverwijzing in veel van deze tests met een Location header.

  3. Een typische test maakt gebruik van de HttpClient en helpermethoden om de aanvraag en het antwoord te verwerken:

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

Elke POST-aanvraag voor de SUT moet voldoen aan de antivervalsingscontrole die automatisch wordt uitgevoerd door het antivervalsingssysteem voor gegevensbeveiliging van de app. Als u de POST-aanvraag van een test wilt regelen, moet de test-app het volgende doen:

  1. Maak een aanvraag voor de pagina.
  2. Parse het antiforgery-token en het aanvraagvalidatietoken uit cookie het antwoord.
  3. Voer de POST-aanvraag uit met de antiforgery- en aanvraagvalidatietokens cookie.

De SendAsync helper-extensiemethoden () en de Helpers/HttpClientExtensions.cs helpermethode (GetDocumentAsyncHelpers/HtmlHelpers.cs) in de voorbeeld-app gebruiken de AngleSharp-parser om de antivervalsingscontrole af te handelen met de volgende methoden:

  • GetDocumentAsync: ontvangt de HttpResponseMessage en retourneert een IHtmlDocument. GetDocumentAsync maakt gebruik van een fabriek die een virtueel antwoord voorbereidt op basis van het oorspronkelijke HttpResponseMessageantwoord. Zie de documentatie van AngleSharp voor meer informatie.
  • SendAsync extensiemethoden voor het HttpClient opstellen van een HttpRequestMessage en aanroep SendAsync(HttpRequestMessage) om aanvragen naar de SUT te verzenden. Overbelastingen voor SendAsync het accepteren van het HTML-formulier (IHtmlFormElement) en het volgende:
    • Knop Verzenden van het formulier (IHtmlElement)
    • Formulierwaardenverzameling (IEnumerable<KeyValuePair<string, string>>)
    • Verzendknop (IHtmlElement) en formulierwaarden (IEnumerable<KeyValuePair<string, string>>)

Opmerking

AngleSharp is een parseringsbibliotheek van derden die wordt gebruikt voor demonstratiedoeleinden in dit onderwerp en de voorbeeld-app. AngleSharp wordt niet ondersteund of vereist voor integratietests van ASP.NET Core-apps. Andere parsers kunnen worden gebruikt, zoals het HTML Agility Pack (HAP). Een andere methode is het schrijven van code voor de directe verwerking van het verificatietoken voor aanvragen van het antivervalsingssysteem en cookie.

Opmerking

De EF-Core in-memory databaseprovider kan worden gebruikt voor beperkte en eenvoudige tests, maar de SQLite-provider is de aanbevolen keuze voor in-memory tests.

De client aanpassen met WithWebHostBuilder

Wanneer er extra configuratie is vereist binnen een testmethode, creëert WithWebHostBuilder een nieuwe WebApplicationFactory met een IWebHostBuilder die verder wordt aangepast door middel van configuratie.

De Post_DeleteMessageHandler_ReturnsRedirectToRoot testmethode van de voorbeeld-app demonstreert het gebruik van WithWebHostBuilder. Met deze test wordt een record verwijderd in de database door een formulierverzending in de SUT te activeren.

Omdat een andere test in de IndexPageTests klasse een bewerking uitvoert waarmee alle records in de database worden verwijderd en die vóór de Post_DeleteMessageHandler_ReturnsRedirectToRoot methode kunnen worden uitgevoerd, wordt de database opnieuw verzonden in deze testmethode om ervoor te zorgen dat er een record aanwezig is om de SUT te verwijderen. Het selecteren van de eerste verwijderknop van het messages formulier in de SUT wordt gesimuleerd in de aanvraag naar de SUT:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                var serviceProvider = services.BuildServiceProvider();

                using (var scope = serviceProvider.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices
                        .GetRequiredService<ApplicationDbContext>();
                    var logger = scopedServices
                        .GetRequiredService<ILogger<IndexPageTests>>();

                    try
                    {
                        Utilities.ReinitializeDbForTests(db);
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, "An error occurred seeding " +
                            "the database with test messages. Error: {Message}", 
                            ex.Message);
                    }
                }
            });
        })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Clientopties

In de volgende tabel ziet u de standaard WebApplicationFactoryClientOptions die beschikbaar is wanneer HttpClient instances worden gemaakt.

Optie Beschrijving Verstek
AllowAutoRedirect Bepaalt of stelt in of HttpClient-instanties automatisch redirect-reacties moeten volgen. true
BaseAddress Hiermee haalt u het basisadres van HttpClient exemplaren op of stelt u dit in. http://localhost
HandleCookies Hiermee haalt u op of stelt u in of HttpClient instanties cookies moeten verwerken. true
MaxAutomaticRedirections Hiermee wordt het maximum aantal omleidingsantwoorden opgehaald of ingesteld dat HttpClient instanties moeten volgen. 7

Maak de WebApplicationFactoryClientOptions klasse en geef deze door aan de CreateClient() methode (standaardwaarden worden weergegeven in het codevoorbeeld):

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

Mock-services injecteren

Services kunnen in een test worden overschreven met een aanroep naar ConfigureTestServices op de host builder. Als u mock-services wilt injecteren, moet de SUT een Startup klasse met een Startup.ConfigureServices methode hebben.

De voorbeeld-SUT bevat een scoped service die een offerte retourneert. Het citaat wordt ingesloten in een verborgen veld op de indexpagina wanneer de indexpagina wordt aangevraagd.

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

De volgende markeringen worden gegenereerd wanneer de SUT-app wordt uitgevoerd:

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

Om de service en offerteinjectie in een integratietest te testen, wordt door de test een mockservice in de SUT geïnjecteerd. De mockservice vervangt de QuoteService van de app door een service die wordt geleverd door de test-app, genaamd TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult<string>(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

ConfigureTestServices wordt aangeroepen en de scoped service wordt geregistreerd:

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

De opmaak die tijdens de uitvoering van de test wordt geproduceerd, weerspiegelt de citaattekst die is opgegeven door TestQuoteService, waardoor de assertie slaagt.

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

Simulatie-authenticatie

Tests in de AuthTests-klasse controleren of een beveiligd eindpunt:

  • Hiermee wordt een niet-geverifieerde gebruiker omgeleid naar de aanmeldingspagina van de app.
  • Retourneert inhoud voor een geverifieerde gebruiker.

In de SUT gebruikt de /SecurePage pagina een AuthorizePage conventie om een AuthorizeFilter op de pagina toe te passen. Zie Razor Autorisatieconventies voor pagina's voor meer informatie.

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

In de Get_SecurePageRedirectsAnUnauthenticatedUser test wordt een WebApplicationFactoryClientOptions ingesteld om omleidingen te verbieden door AllowAutoRedirect op false in te stellen.

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

Door te voorkomen dat de client de omleiding volgt, kunnen de volgende controles worden uitgevoerd:

  • De statuscode die door de SUT wordt geretourneerd, kan worden gecontroleerd op basis van het verwachte HttpStatusCode.Redirect resultaat, niet de uiteindelijke statuscode na de omleiding naar de aanmeldingspagina, wat zou zijn HttpStatusCode.OK.
  • De Location headerwaarde in de antwoordheaders wordt gecontroleerd om te bevestigen dat deze begint met http://localhost/Identity/Account/Login, niet het laatste antwoord op de aanmeldingspagina, waar de Location header niet aanwezig zou zijn.

De testapp kan een AuthenticationHandler<TOptions> in ConfigureTestServices simuleren om aspecten van authenticatie en autorisatie te testen. Een minimaal scenario retourneert een AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, 
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "Test");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

De TestAuthHandler wordt aangeroepen om een gebruiker te authenticeren wanneer het verificatieschema is ingesteld op Test waar AddAuthentication geregistreerd is voor ConfigureTestServices. Het is belangrijk dat het Test schema overeenkomt met het schema dat uw app verwacht. Anders werkt verificatie niet.

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

Zie de sectie WebApplicationFactoryClientOptions voor meer informatie.

De omgeving instellen

Standaard is de host- en app-omgeving van de SUT geconfigureerd voor het gebruik van de ontwikkelomgeving. De systeemomgeving van de SUT overschrijven bij gebruik van IHostBuilder:

  • Stel de ASPNETCORE_ENVIRONMENT omgevingsvariabele in (bijvoorbeeld Staging, Productionof andere aangepaste waarde, zoals Testing).
  • Overschrijven CreateHostBuilder in de test-app om omgevingsvariabelen te lezen die voorafgegaan zijn door ASPNETCORE.
protected override IHostBuilder CreateHostBuilder() =>
    base.CreateHostBuilder()
        .ConfigureHostConfiguration(
            config => config.AddEnvironmentVariables("ASPNETCORE"));

Als de SUT gebruikmaakt van de webhost (IWebHostBuilder), overschrijft u CreateWebHostBuilder:

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

Hoe de testinfrastructuur het app-inhoudsbasispad afleidt

De WebApplicationFactory constructor bepaalt het hoofdpad van de app-inhoud door te zoeken naar een WebApplicationFactoryContentRootAttribute op de assembly met de integratietests met een sleutel die gelijk is aan de TEntryPoint assembly System.Reflection.Assembly.FullName. Als een kenmerk met de juiste sleutel niet wordt gevonden, wordt overgeschakeld naar het zoeken naar een solutionbestand (WebApplicationFactory) en wordt de assemblynaam toegevoegd aan de solution directory. De hoofdmap van de app (het hoofdpad van de inhoud) wordt gebruikt om weergaven en inhoudsbestanden te detecteren.

Schaduwkopie uitschakelen

Schaduwkopie zorgt ervoor dat de tests worden uitgevoerd in een andere map dan de uitvoermap. Als uw tests afhankelijk zijn van het laden van bestanden ten opzichte Assembly.Location van en u problemen ondervindt, moet u mogelijk schaduwkopie uitschakelen.

Als u schaduwkopie wilt uitschakelen wanneer u xUnit gebruikt, maakt u een xunit.runner.json bestand in uw testprojectmap met de juiste configuratie-instelling:

{
  "shadowCopy": false
}

Verwijdering van objecten

Nadat de tests van de IClassFixture-implementatie zijn uitgevoerd, worden TestServer en HttpClient verwijderd wanneer xUnit de WebApplicationFactory verwijdert. Als objecten die door de ontwikkelaar zijn geïnstantieerd, verwijdering vereisen, moet u deze verwijderen in de IClassFixture implementatie. Zie Een verwijderingsmethode implementeren voor meer informatie.

Voorbeeld van integratietests

De voorbeeld-app bestaat uit twee apps:

Applicatie Projectmap Beschrijving
Bericht-app (de SUT) src/RazorPagesProject Hiermee kan een gebruiker berichten toevoegen, verwijderen, alles verwijderen en analyseren.
App testen tests/RazorPagesProject.Tests Wordt gebruikt om de integratietest van de SUT uit te voeren.

De tests kunnen worden uitgevoerd met behulp van de ingebouwde testfuncties van een IDE, zoals Visual Studio. Als u Visual Studio Code of de opdrachtregel gebruikt, voert u de volgende opdracht uit bij een opdrachtprompt in de tests/RazorPagesProject.Tests map:

dotnet test

Organisatie van de berichtenapp (SUT)

De SUT is een Razor paginaberichtsysteem met de volgende kenmerken:

  • De indexpagina van de app (Pages/Index.cshtml en Pages/Index.cshtml.cs) biedt een ui- en paginamodelmethode om de toevoeging, verwijdering en analyse van berichten (gemiddelde woorden per bericht) te beheren.
  • Een bericht wordt beschreven door de Message klasse (Data/Message.cs) met twee eigenschappen: Id (sleutel) en Text (bericht). De Text eigenschap is vereist en beperkt tot 200 tekens.
  • Berichten worden opgeslagen met behulp van de in-memory database van Entity Framework†.
  • De app bevat een DATA Access Layer (DAL) in de databasecontextklasse (AppDbContextData/AppDbContext.cs).
  • Als de database leeg is bij het opstarten van de app, wordt het berichtenarchief geïnitialiseerd met drie berichten.
  • De app bevat een /SecurePage die alleen toegankelijk is voor een geauthenticeerde gebruiker.

†De EF-onderwerp, Testen met InMemory, legt uit hoe u een in-memory database gebruikt voor tests met MSTest. In dit onderwerp wordt het xUnit-testframework gebruikt. Testconcepten en test-implementaties in verschillende testframeworks zijn vergelijkbaar, maar niet identiek.

Hoewel de app geen gebruik maakt van het opslagplaatspatroon en geen effectief voorbeeld is van het UoW-patroon (Unit of Work),Razor ondersteunt Pages deze patronen van ontwikkeling. Zie Voor meer informatie het ontwerpen van de infrastructuurpersistentielaag en testcontrollerlogica (het voorbeeld implementeert het opslagplaatspatroon).

App-organisatie testen

De test-app is een console-app in de tests/RazorPagesProject.Tests map.

App-map testen Beschrijving
AuthTests Bevat testmethoden voor:
  • Toegang tot een beveiligde pagina door een niet-geverifieerde gebruiker.
  • Toegang tot een beveiligde pagina door een geverifieerde gebruiker met een mock AuthenticationHandler<TOptions>.
  • Het verkrijgen van een GitHub-gebruikersprofiel en het controleren van de gebruikersaanmelding van het profiel.
BasicTests Bevat een testmethode voor routering en inhoudstype.
IntegrationTests Bevat de integratietests voor de pagina Index met behulp van aangepaste WebApplicationFactory klasse.
Helpers/Utilities
  • Utilities.cs bevat de InitializeDbForTests methode die wordt gebruikt om de database te seeden met testgegevens.
  • HtmlHelpers.cs biedt een methode voor het retourneren van een AngleSharp IHtmlDocument voor gebruik in de testmethoden.
  • HttpClientExtensions.cs biedt overbelastingen voor SendAsync het indienen van aanvragen bij de SUT.

Het testframework is xUnit. Integratietests worden uitgevoerd met de Microsoft.AspNetCore.TestHost, die de TestServer omvat. Omdat het Microsoft.AspNetCore.Mvc.Testing pakket wordt gebruikt voor het configureren van de testhost en testserver, vereisen de pakketten TestHost en TestServer geen directe pakketverwijzingen in het projectbestand van de test-app of in de configuratie voor de ontwikkelaar.

Integratietests vereisen meestal een kleine gegevensset in de database voordat de test wordt uitgevoerd. Met een testoproep voor verwijderen van een databaserecord moet de database bijvoorbeeld ten minste één record hebben om de verwijderaanvraag te laten slagen.

De voorbeeld-app vult de database met drie berichten in Utilities.cs die tests kunnen gebruiken wanneer ze worden uitgevoerd.

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

De databasecontext van de SUT wordt geregistreerd in de Startup.ConfigureServices methode. De callback van de test-app builder.ConfigureServices wordt uitgevoerd nadat de code van de app Startup.ConfigureServices is uitgevoerd. Als u een andere database voor de tests wilt gebruiken, moet de databasecontext van de app worden vervangen in builder.ConfigureServices. Zie de sectie WebApplicationFactory aanpassen voor meer informatie.

Voor SUT's die nog steeds gebruikmaken van de webhost, wordt de callback van builder.ConfigureServices de test-app uitgevoerd vóór de code van Startup.ConfigureServices de SUT. De callback van builder.ConfigureTestServices de test-app wordt uitgevoerd.

Aanvullende bronnen

In dit artikel wordt ervan uitgegaan dat u basiskennis hebt van eenheidstests. Als u niet bekend bent met testconcepten, raadpleegt u het artikel Unit Testing in .NET Core en .NET Standard en de bijbehorende gekoppelde inhoud.

Voorbeeldcode bekijken of downloaden (hoe download je)

De voorbeeld-app is een Razor Pagina-app en gaat uit van basiskennis van Razor Pages. Als u niet bekend bent met Razor Pagina's, raadpleegt u de volgende artikelen:

Voor het testen van SPA's raden we een hulpprogramma aan, zoals Playwright voor .NET, waarmee een browser kan worden geautomatiseerd.

Inleiding tot integratietests

Integratietests evalueren de onderdelen van een app op een breder niveau dan eenheidstests. Eenheidstests worden gebruikt voor het testen van geïsoleerde softwareonderdelen, zoals afzonderlijke klassemethoden. Integratietests bevestigen dat twee of meer app-onderdelen samenwerken om een verwacht resultaat te produceren, mogelijk inclusief elk onderdeel dat nodig is om een aanvraag volledig te verwerken.

Deze bredere tests worden gebruikt om de infrastructuur en het hele framework van de app te testen, vaak inclusief de volgende onderdelen:

  • Gegevensbank
  • Bestandssysteem
  • Netwerkapparaten
  • Pijplijn voor aanvraag-antwoord

Eenheidstests maken gebruik van ge fabriceerde onderdelen, ook wel nep- of mockobjecten genoemd, in plaats van infrastructuuronderdelen.

In tegenstelling tot eenheidstests, integratietests:

  • Gebruik de werkelijke onderdelen die door de app in productie worden gebruikt.
  • Meer code en gegevensverwerking vereisen.
  • Het duurt langer om uit te voeren.

Beperk daarom het gebruik van integratietests tot de belangrijkste infrastructuurscenario's. Als een gedrag kan worden getest met behulp van een eenheidstest of een integratietest, kiest u de eenheidstest.

In discussies over integratietests wordt het geteste project kortom het systeem onder test of 'SUT' genoemd. "SUT" wordt in dit artikel gebruikt om te verwijzen naar de ASP.NET Core-app die wordt getest.

Schrijf geen integratietests voor elke permutatie van gegevens- en bestandstoegang met databases en bestandssystemen. Ongeacht het aantal plaatsen in een app dat communiceert met databases en bestandssystemen, is een gerichte set integratietests voor lezen, schrijven, bijwerken en verwijderen meestal geschikt voor het testen van database- en bestandssysteemonderdelen. Gebruik eenheidstests voor routinetests van methodelogica die met deze onderdelen communiceren. In eenheidstests resulteert het gebruik van infrastructuurvervalsingen of mocks in een snellere testuitvoering.

ASP.NET Kernintegratietests

Integratietests in ASP.NET Core vereisen het volgende:

  • Een testproject wordt gebruikt om de tests te bevatten en uit te voeren. Het testproject heeft een verwijzing naar de SUT.
  • Het testproject maakt een testwebhost voor de SUT en gebruikt een testserverclient voor het afhandelen van aanvragen en antwoorden met de SUT.
  • Een testloper wordt gebruikt om de tests uit te voeren en de testresultaten te rapporteren.

Integratietests volgen een reeks gebeurtenissen die de gebruikelijke teststappen Rangschikken, Act en Assert bevatten:

  1. De webhost van de SUT is geconfigureerd.
  2. Er wordt een testserverclient gemaakt om aanvragen naar de app te verzenden.
  3. De teststap Rangschikken wordt uitgevoerd: De test-app bereidt een aanvraag voor.
  4. De teststap Act wordt uitgevoerd: de client verzendt de aanvraag en ontvangt het antwoord.
  5. De assert-teststap wordt uitgevoerd: het daadwerkelijke antwoord wordt gevalideerd als een geslaagd of mislukt op basis van een verwacht antwoord.
  6. Het proces wordt voortgezet totdat alle tests worden uitgevoerd.
  7. De testresultaten worden gerapporteerd.

Normaal gesproken is de testwebhost anders geconfigureerd dan de normale webhost van de app voor de testuitvoeringen. Een andere database of andere app-instellingen kunnen bijvoorbeeld worden gebruikt voor de tests.

Infrastructuuronderdelen, zoals de testwebhost en de testserver in het geheugen (TestServer), worden geleverd of beheerd door het microsoft.AspNetCore.Mvc.Testing-pakket . Het gebruik van dit pakket stroomlijnt het maken en uitvoeren van tests.

Het Microsoft.AspNetCore.Mvc.Testing pakket verwerkt de volgende taken:

  • Kopieert het afhankelijkhedenbestand (.deps) van de SUT naar de map van bin het testproject.
  • Hiermee stelt u de inhoudshoofdmap in op de hoofdmap van het SUT-project, zodat statische bestanden en pagina's/weergaven worden gevonden wanneer de tests worden uitgevoerd.
  • Biedt de klasse WebApplicationFactory om het bootstrappen van de SUT te stroomlijnen.TestServer

In de documentatie voor eenheidstests wordt beschreven hoe u een testproject en testloper instelt, samen met gedetailleerde instructies voor het uitvoeren van tests en aanbevelingen voor het benoemen van tests en testklassen.

Afzonderlijke eenheidstests van integratietests in verschillende projecten. Het scheiden van de tests:

  • Helpt ervoor te zorgen dat onderdelen voor infrastructuurtests niet per ongeluk worden opgenomen in de eenheidstests.
  • Hiermee kunt u bepalen welke set tests worden uitgevoerd.

Er is vrijwel geen verschil tussen de configuratie voor tests van Razor Pagina-apps en MVC-apps. Het enige verschil is in de naam van de tests. In een Razor Pagina-app worden tests van pagina-eindpunten meestal vernoemd naar de paginamodelklasse (bijvoorbeeld IndexPageTests om de integratie van onderdelen voor de indexpagina te testen). In een MVC-app worden tests meestal ingedeeld op controllerklassen en benoemd naar de controllers die ze testen (bijvoorbeeld HomeControllerTests om de integratie van onderdelen voor de Home controller te testen).

Vereisten voor de app testen

Het testproject moet:

Deze vereisten kunnen worden weergegeven in de voorbeeld-app. Inspecteer het tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj bestand. De voorbeeld-app maakt gebruik van het xUnit-testframework en de AngleSharp-parserbibliotheek , dus de voorbeeld-app verwijst ook naar:

In apps die versie 2.4.2 of hoger gebruiken xunit.runner.visualstudio , moet het testproject verwijzen naar het Microsoft.NET.Test.Sdk pakket.

Entity Framework Core wordt ook gebruikt in de tests. Bekijk het projectbestand in GitHub.

SUT-omgeving

Als de omgeving van de SUT niet is ingesteld, wordt de omgeving standaard ingesteld op Ontwikkeling.

Basistests met de standaard WebApplicationFactory

Maak de impliciet gedefinieerde Program klasse beschikbaar voor het testproject door een van de volgende handelingen uit te voeren:

  • Interne typen van de web-app beschikbaar maken voor het testproject. Dit kan worden gedaan in het bestand van het SUT-project (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Maak de Program klasse openbaar met behulp van een gedeeltelijke klassedeclaratie :

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

    De voorbeeld-app maakt gebruik van de Program benadering van gedeeltelijke klassen.

WebApplicationFactory<TEntryPoint> wordt gebruikt om een TestServer voor de integratietests te maken. TEntryPoint is de ingangspuntklasse van de SUT, meestal Program.cs.

Testklassen implementeren een interface voor klassearmaturen (IClassFixture) om aan te geven dat de klasse tests bevat en gedeelde objectexemplaren biedt voor de tests in de klasse.

De volgende testklasse, BasicTests, gebruikt de WebApplicationFactory om de SUT te bootstrapen en verstrekt een HttpClient aan een testmethode, Get_EndpointsReturnSuccessAndCorrectContentType. De methode controleert of de antwoordstatuscode succesvol is (200-299) en of de Content-Type header text/html; charset=utf-8 is voor verschillende app-pagina's.

CreateClient() maakt een instantie van HttpClient dat automatisch omleidingen volgt en cookies verwerkt.

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

Standaard worden niet-essentiële cookies niet bewaard voor aanvragen wanneer het toestemmingsbeleid voor algemene gegevensbescherming is ingeschakeld. Als u niet-essentiële cookies wilt behouden, zoals cookies die door de TempData-provider worden gebruikt, markeert u deze als essentieel in uw tests. Zie cookie voor instructies over het markeren van een als essentieel onderdeel.

AngleSharp versus Application Parts voor antiforgery-controlesystemen

In dit artikel wordt de AngleSharp-parser gebruikt om de antivervalsingcontroles af te handelen door pagina's te laden en de HTML te parseren. Voor het testen van de eindpunten van controller- en Razor paginaweergaven op een lager niveau, zonder te zorgen over hoe ze in de browser worden weergegeven, kunt u overwegen om te gebruiken Application Parts. De benadering toepassingsonderdelen injecteert een controller of Razor pagina in de app die kan worden gebruikt om JSON-aanvragen te maken om de vereiste waarden op te halen. Zie de blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts and associated GitHub repo by Martin Costello (Engelstalig) voor meer informatie.

WebApplicationFactory aanpassen

De configuratie van de webhost kan onafhankelijk van de testklassen worden gemaakt door te erven van WebApplicationFactory<TEntryPoint> om een of meer aangepaste fabrieken te creëren.

  1. Overnemen van WebApplicationFactory en overschrijven ConfigureWebHost. Met IWebHostBuilder kan de serviceverzameling worden geconfigureerd met IWebHostBuilder.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");
        }
    }
    

    Database-seeding in de voorbeeld-app wordt uitgevoerd door de InitializeDbForTests methode. De methode wordt beschreven in het voorbeeld van integratietests: sectie App-organisatie testen .

    De databasecontext van de SUT is geregistreerd in Program.cs. De callback van de test-app builder.ConfigureServices wordt uitgevoerd nadat de code van de app Program.cs is uitgevoerd. Als u een andere database wilt gebruiken voor de tests dan de database van de app, moet de databasecontext van de app worden vervangen in builder.ConfigureServices.

    De voorbeeld-app vindt de servicedescriptor voor de databasecontext en gebruikt de descriptor om de serviceregistratie te verwijderen. De factory voegt vervolgens een nieuwe ApplicationDbContext toe die gebruikmaakt van een in-memory database voor de tests.

    Als u verbinding wilt maken met een andere database, wijzigt u de DbConnection. Een SQL Server-testdatabase gebruiken:

  1. Gebruik de aangepaste CustomWebApplicationFactory in testklassen. In het volgende voorbeeld wordt de factory in de IndexPageTests klasse gebruikt:

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

    De client van de voorbeeld-app is zo geconfigureerd dat het HttpClient geen omleidingen volgt. Zoals verderop in de sectie Mock-verificatie wordt uitgelegd, kunnen tests het resultaat van het eerste antwoord van de app controleren. De eerste reactie is een doorverwijzing in veel van deze tests met een Location header.

  2. Een typische test maakt gebruik van de HttpClient en helpermethoden om de aanvraag en het antwoord te verwerken:

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

Elke POST-aanvraag voor de SUT moet voldoen aan de antivervalsingscontrole die automatisch wordt uitgevoerd door het antivervalsingssysteem voor gegevensbeveiliging van de app. Als u de POST-aanvraag van een test wilt regelen, moet de test-app het volgende doen:

  1. Maak een aanvraag voor de pagina.
  2. Parse het antiforgery-token en het aanvraagvalidatietoken uit cookie het antwoord.
  3. Voer de POST-aanvraag uit met de antiforgery- en aanvraagvalidatietokens cookie.

De SendAsync helper-extensiemethoden () en de Helpers/HttpClientExtensions.cs helpermethode (GetDocumentAsyncHelpers/HtmlHelpers.cs) in de voorbeeld-app gebruiken de AngleSharp-parser om de antivervalsingscontrole af te handelen met de volgende methoden:

  • GetDocumentAsync: ontvangt de HttpResponseMessage en retourneert een IHtmlDocument. GetDocumentAsync maakt gebruik van een fabriek die een virtueel antwoord voorbereidt op basis van het oorspronkelijke HttpResponseMessageantwoord. Zie de documentatie van AngleSharp voor meer informatie.
  • SendAsync extensiemethoden voor het HttpClient opstellen van een HttpRequestMessage en aanroep SendAsync(HttpRequestMessage) om aanvragen naar de SUT te verzenden. Overbelastingen voor SendAsync het accepteren van het HTML-formulier (IHtmlFormElement) en het volgende:
    • Knop Verzenden van het formulier (IHtmlElement)
    • Formulierwaardenverzameling (IEnumerable<KeyValuePair<string, string>>)
    • Verzendknop (IHtmlElement) en formulierwaarden (IEnumerable<KeyValuePair<string, string>>)

AngleSharp is een parseringsbibliotheek van derden die wordt gebruikt voor demonstratiedoeleinden in dit artikel en de voorbeeld-app. AngleSharp wordt niet ondersteund of vereist voor integratietests van ASP.NET Core-apps. Andere parsers kunnen worden gebruikt, zoals het HTML Agility Pack (HAP). Een andere methode is het schrijven van code voor de directe verwerking van het verificatietoken voor aanvragen van het antivervalsingssysteem en cookie. Zie in dit artikel AngleSharp versus Application Parts antiforgery-controles voor meer informatie.

De EF-Core in-memory databaseprovider kan worden gebruikt voor beperkte en eenvoudige tests, maar de SQLite-provider is de aanbevolen keuze voor in-memory tests.

Zie Opstarten uitbreiden met opstartfilters die laten zien hoe u middleware configureert met behulp van IStartupFilter, wat handig is wanneer een test een aangepaste service of middleware vereist.

De client aanpassen met WithWebHostBuilder

Wanneer er extra configuratie is vereist binnen een testmethode, creëert WithWebHostBuilder een nieuwe WebApplicationFactory met een IWebHostBuilder die verder wordt aangepast door middel van configuratie.

De voorbeeldcode roept WithWebHostBuilder om ingerichte services te vervangen door test-stubs. Zie Mock-services injecteren in dit artikel voor meer informatie en voorbeeldgebruik.

De Post_DeleteMessageHandler_ReturnsRedirectToRoot testmethode van de voorbeeld-app demonstreert het gebruik van WithWebHostBuilder. Met deze test wordt een record verwijderd in de database door een formulierverzending in de SUT te activeren.

Omdat een andere test in de IndexPageTests klasse een bewerking uitvoert waarmee alle records in de database worden verwijderd en die vóór de Post_DeleteMessageHandler_ReturnsRedirectToRoot methode kunnen worden uitgevoerd, wordt de database opnieuw verzonden in deze testmethode om ervoor te zorgen dat er een record aanwezig is om de SUT te verwijderen. Het selecteren van de eerste verwijderknop van het messages formulier in de SUT wordt gesimuleerd in de aanvraag naar de SUT:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Clientopties

Zie de WebApplicationFactoryClientOptions pagina voor standaardinstellingen en beschikbare opties bij het maken van HttpClient exemplaren.

Maak de WebApplicationFactoryClientOptions klasse en geef deze door aan de 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
        });
    }

NOTITIE: Als u HTTPS-omleidingswaarschuwingen in logboeken wilt voorkomen bij het gebruik van HTTPS Redirection Middleware, stelt u BaseAddress = new Uri("https://localhost")

Mock-services injecteren

Services kunnen in een test worden overschreven met een aanroep naar ConfigureTestServices op de host builder. Als u het bereik van de overschreven services voor de test zelf wilt instellen, wordt de WithWebHostBuilder methode gebruikt om een hostbouwer op te halen. Dit is te zien in de volgende tests:

De voorbeeld-SUT bevat een scoped service die een offerte retourneert. Het citaat wordt ingesloten in een verborgen veld op de indexpagina wanneer de indexpagina wordt aangevraagd.

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

De volgende markeringen worden gegenereerd wanneer de SUT-app wordt uitgevoerd:

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

Om de service en offerteinjectie in een integratietest te testen, wordt door de test een mockservice in de SUT geïnjecteerd. De mockservice vervangt de QuoteService van de app door een service die wordt geleverd door de test-app, genaamd TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

ConfigureTestServices wordt aangeroepen en de scoped service wordt geregistreerd:

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

De opmaak die tijdens de uitvoering van de test wordt geproduceerd, weerspiegelt de citaattekst die is opgegeven door TestQuoteService, waardoor de assertie slaagt.

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

Simulatie-authenticatie

Tests in de AuthTests-klasse controleren of een beveiligd eindpunt:

  • Hiermee wordt een niet-geverifieerde gebruiker omgeleid naar de aanmeldingspagina van de app.
  • Retourneert inhoud voor een geverifieerde gebruiker.

In de SUT gebruikt de /SecurePage pagina een AuthorizePage conventie om een AuthorizeFilter op de pagina toe te passen. Zie Razor Autorisatieconventies voor pagina's voor meer informatie.

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

In de Get_SecurePageRedirectsAnUnauthenticatedUser test wordt een WebApplicationFactoryClientOptions ingesteld om omleidingen te verbieden door AllowAutoRedirect op false in te stellen.

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

Door te voorkomen dat de client de omleiding volgt, kunnen de volgende controles worden uitgevoerd:

  • De statuscode die door de SUT wordt geretourneerd, kan worden gecontroleerd op basis van het verwachte HttpStatusCode.Redirect resultaat, niet de uiteindelijke statuscode na de omleiding naar de aanmeldingspagina, wat zou zijn HttpStatusCode.OK.
  • De Location headerwaarde in de antwoordheaders wordt gecontroleerd om te bevestigen dat deze begint met http://localhost/Identity/Account/Login, niet het laatste antwoord op de aanmeldingspagina, waar de Location header niet aanwezig zou zijn.

De testapp kan een AuthenticationHandler<TOptions> in ConfigureTestServices simuleren om aspecten van authenticatie en autorisatie te testen. Een minimaal scenario retourneert een AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
        : base(options, logger, encoder, clock)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

De TestAuthHandler wordt aangeroepen om een gebruiker te authenticeren wanneer het verificatieschema is ingesteld op TestScheme waar AddAuthentication geregistreerd is voor ConfigureTestServices. Het is belangrijk dat het TestScheme schema overeenkomt met het schema dat uw app verwacht. Anders werkt verificatie niet.

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

Zie de sectie WebApplicationFactoryClientOptions voor meer informatie.

Basistests voor verificatie-middleware

Bekijk deze GitHub-opslagplaats voor basistests van verificatie-middleware. Het bevat een testserver die specifiek is voor het testscenario.

De omgeving instellen

Stel de omgeving in de aangepaste applicatiefabriek in.

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

Hoe de testinfrastructuur het app-inhoudsbasispad afleidt

De WebApplicationFactory constructor bepaalt het hoofdpad van de app-inhoud door te zoeken naar een WebApplicationFactoryContentRootAttribute op de assembly met de integratietests met een sleutel die gelijk is aan de TEntryPoint assembly System.Reflection.Assembly.FullName. Als een kenmerk met de juiste sleutel niet wordt gevonden, wordt overgeschakeld naar het zoeken naar een solutionbestand (WebApplicationFactory) en wordt de assemblynaam toegevoegd aan de solution directory. De hoofdmap van de app (het hoofdpad van de inhoud) wordt gebruikt om weergaven en inhoudsbestanden te detecteren.

Schaduwkopie uitschakelen

Schaduwkopie zorgt ervoor dat de tests worden uitgevoerd in een andere map dan de uitvoermap. Als uw tests afhankelijk zijn van het laden van bestanden ten opzichte Assembly.Location van en u problemen ondervindt, moet u mogelijk schaduwkopie uitschakelen.

Als u schaduwkopie wilt uitschakelen wanneer u xUnit gebruikt, maakt u een xunit.runner.json bestand in uw testprojectmap met de juiste configuratie-instelling:

{
  "shadowCopy": false
}

Verwijdering van objecten

Nadat de tests van de IClassFixture-implementatie zijn uitgevoerd, worden TestServer en HttpClient verwijderd wanneer xUnit de WebApplicationFactory verwijdert. Als objecten die door de ontwikkelaar zijn geïnstantieerd, verwijdering vereisen, moet u deze verwijderen in de IClassFixture implementatie. Zie Een verwijderingsmethode implementeren voor meer informatie.

Voorbeeld van integratietests

De voorbeeld-app bestaat uit twee apps:

Applicatie Projectmap Beschrijving
Bericht-app (de SUT) src/RazorPagesProject Hiermee kan een gebruiker berichten toevoegen, verwijderen, alles verwijderen en analyseren.
App testen tests/RazorPagesProject.Tests Wordt gebruikt om de integratietest van de SUT uit te voeren.

De tests kunnen worden uitgevoerd met behulp van de ingebouwde testfuncties van een IDE, zoals Visual Studio. Als u Visual Studio Code of de opdrachtregel gebruikt, voert u de volgende opdracht uit bij een opdrachtprompt in de tests/RazorPagesProject.Tests map:

dotnet test

Organisatie van de berichtenapp (SUT)

De SUT is een Razor paginaberichtsysteem met de volgende kenmerken:

  • De indexpagina van de app (Pages/Index.cshtml en Pages/Index.cshtml.cs) biedt een ui- en paginamodelmethode om de toevoeging, verwijdering en analyse van berichten (gemiddelde woorden per bericht) te beheren.
  • Een bericht wordt beschreven door de Message klasse (Data/Message.cs) met twee eigenschappen: Id (sleutel) en Text (bericht). De Text eigenschap is vereist en beperkt tot 200 tekens.
  • Berichten worden opgeslagen met behulp van de in-memory database van Entity Framework†.
  • De app bevat een DATA Access Layer (DAL) in de databasecontextklasse (AppDbContextData/AppDbContext.cs).
  • Als de database leeg is bij het opstarten van de app, wordt het berichtenarchief geïnitialiseerd met drie berichten.
  • De app bevat een /SecurePage die alleen toegankelijk is voor een geauthenticeerde gebruiker.

†De EF-artikel, Testen met InMemory, legt uit hoe u een in-memory database gebruikt voor tests met MSTest. In dit onderwerp wordt het xUnit-testframework gebruikt. Testconcepten en test-implementaties in verschillende testframeworks zijn vergelijkbaar, maar niet identiek.

Hoewel de app geen gebruik maakt van het opslagplaatspatroon en geen effectief voorbeeld is van het UoW-patroon (Unit of Work),Razor ondersteunt Pages deze patronen van ontwikkeling. Zie Voor meer informatie het ontwerpen van de infrastructuurpersistentielaag en testcontrollerlogica (het voorbeeld implementeert het opslagplaatspatroon).

App-organisatie testen

De test-app is een console-app in de tests/RazorPagesProject.Tests map.

App-map testen Beschrijving
AuthTests Bevat testmethoden voor:
  • Toegang tot een beveiligde pagina door een niet-geverifieerde gebruiker.
  • Toegang tot een beveiligde pagina door een geverifieerde gebruiker met een mock AuthenticationHandler<TOptions>.
  • Het verkrijgen van een GitHub-gebruikersprofiel en het controleren van de gebruikersaanmelding van het profiel.
BasicTests Bevat een testmethode voor routering en inhoudstype.
IntegrationTests Bevat de integratietests voor de pagina Index met behulp van aangepaste WebApplicationFactory klasse.
Helpers/Utilities
  • Utilities.cs bevat de InitializeDbForTests methode die wordt gebruikt om de database te seeden met testgegevens.
  • HtmlHelpers.cs biedt een methode voor het retourneren van een AngleSharp IHtmlDocument voor gebruik in de testmethoden.
  • HttpClientExtensions.cs biedt overbelastingen voor SendAsync het indienen van aanvragen bij de SUT.

Het testframework is xUnit. Integratietests worden uitgevoerd met de Microsoft.AspNetCore.TestHost, die de TestServer omvat. Omdat het Microsoft.AspNetCore.Mvc.Testing pakket wordt gebruikt voor het configureren van de testhost en testserver, vereisen de pakketten TestHost en TestServer geen directe pakketverwijzingen in het projectbestand van de test-app of in de configuratie voor de ontwikkelaar.

Integratietests vereisen meestal een kleine gegevensset in de database voordat de test wordt uitgevoerd. Met een testoproep voor verwijderen van een databaserecord moet de database bijvoorbeeld ten minste één record hebben om de verwijderaanvraag te laten slagen.

De voorbeeld-app vult de database met drie berichten in Utilities.cs die tests kunnen gebruiken wanneer ze worden uitgevoerd.

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

De databasecontext van de SUT is geregistreerd in Program.cs. De callback van de test-app builder.ConfigureServices wordt uitgevoerd nadat de code van de app Program.cs is uitgevoerd. Als u een andere database voor de tests wilt gebruiken, moet de databasecontext van de app worden vervangen in builder.ConfigureServices. Zie de sectie WebApplicationFactory aanpassen voor meer informatie.

Aanvullende bronnen

In dit artikel wordt ervan uitgegaan dat u basiskennis hebt van eenheidstests. Als u niet bekend bent met testconcepten, raadpleegt u het artikel Unit Testing in .NET Core en .NET Standard en de bijbehorende gekoppelde inhoud.

Voorbeeldcode bekijken of downloaden (hoe download je)

De voorbeeld-app is een Razor Pagina-app en gaat uit van basiskennis van Razor Pages. Als u niet bekend bent met Razor Pagina's, raadpleegt u de volgende artikelen:

Voor het testen van SPA's raden we een hulpprogramma aan, zoals Playwright voor .NET, waarmee een browser kan worden geautomatiseerd.

Inleiding tot integratietests

Integratietests evalueren de onderdelen van een app op een breder niveau dan eenheidstests. Eenheidstests worden gebruikt voor het testen van geïsoleerde softwareonderdelen, zoals afzonderlijke klassemethoden. Integratietests bevestigen dat twee of meer app-onderdelen samenwerken om een verwacht resultaat te produceren, mogelijk inclusief elk onderdeel dat nodig is om een aanvraag volledig te verwerken.

Deze bredere tests worden gebruikt om de infrastructuur en het hele framework van de app te testen, vaak inclusief de volgende onderdelen:

  • Gegevensbank
  • Bestandssysteem
  • Netwerkapparaten
  • Pijplijn voor aanvraag-antwoord

Eenheidstests maken gebruik van ge fabriceerde onderdelen, ook wel nep- of mockobjecten genoemd, in plaats van infrastructuuronderdelen.

In tegenstelling tot eenheidstests, integratietests:

  • Gebruik de werkelijke onderdelen die door de app in productie worden gebruikt.
  • Meer code en gegevensverwerking vereisen.
  • Het duurt langer om uit te voeren.

Beperk daarom het gebruik van integratietests tot de belangrijkste infrastructuurscenario's. Als een gedrag kan worden getest met behulp van een eenheidstest of een integratietest, kiest u de eenheidstest.

In discussies over integratietests wordt het geteste project kortom het systeem onder test of 'SUT' genoemd. "SUT" wordt in dit artikel gebruikt om te verwijzen naar de ASP.NET Core-app die wordt getest.

Schrijf geen integratietests voor elke permutatie van gegevens- en bestandstoegang met databases en bestandssystemen. Ongeacht het aantal plaatsen in een app dat communiceert met databases en bestandssystemen, is een gerichte set integratietests voor lezen, schrijven, bijwerken en verwijderen meestal geschikt voor het testen van database- en bestandssysteemonderdelen. Gebruik eenheidstests voor routinetests van methodelogica die met deze onderdelen communiceren. In eenheidstests resulteert het gebruik van infrastructuurvervalsingen of mocks in een snellere testuitvoering.

ASP.NET Kernintegratietests

Integratietests in ASP.NET Core vereisen het volgende:

  • Een testproject wordt gebruikt om de tests te bevatten en uit te voeren. Het testproject heeft een verwijzing naar de SUT.
  • Het testproject maakt een testwebhost voor de SUT en gebruikt een testserverclient voor het afhandelen van aanvragen en antwoorden met de SUT.
  • Een testloper wordt gebruikt om de tests uit te voeren en de testresultaten te rapporteren.

Integratietests volgen een reeks gebeurtenissen die de gebruikelijke teststappen Rangschikken, Act en Assert bevatten:

  1. De webhost van de SUT is geconfigureerd.
  2. Er wordt een testserverclient gemaakt om aanvragen naar de app te verzenden.
  3. De teststap Rangschikken wordt uitgevoerd: De test-app bereidt een aanvraag voor.
  4. De teststap Act wordt uitgevoerd: de client verzendt de aanvraag en ontvangt het antwoord.
  5. De assert-teststap wordt uitgevoerd: het daadwerkelijke antwoord wordt gevalideerd als een geslaagd of mislukt op basis van een verwacht antwoord.
  6. Het proces wordt voortgezet totdat alle tests worden uitgevoerd.
  7. De testresultaten worden gerapporteerd.

Normaal gesproken is de testwebhost anders geconfigureerd dan de normale webhost van de app voor de testuitvoeringen. Een andere database of andere app-instellingen kunnen bijvoorbeeld worden gebruikt voor de tests.

Infrastructuuronderdelen, zoals de testwebhost en de testserver in het geheugen (TestServer), worden geleverd of beheerd door het microsoft.AspNetCore.Mvc.Testing-pakket . Het gebruik van dit pakket stroomlijnt het maken en uitvoeren van tests.

Het Microsoft.AspNetCore.Mvc.Testing pakket verwerkt de volgende taken:

  • Kopieert het afhankelijkhedenbestand (.deps) van de SUT naar de map van bin het testproject.
  • Hiermee stelt u de inhoudshoofdmap in op de hoofdmap van het SUT-project, zodat statische bestanden en pagina's/weergaven worden gevonden wanneer de tests worden uitgevoerd.
  • Biedt de klasse WebApplicationFactory om het bootstrappen van de SUT te stroomlijnen.TestServer

In de documentatie voor eenheidstests wordt beschreven hoe u een testproject en testloper instelt, samen met gedetailleerde instructies voor het uitvoeren van tests en aanbevelingen voor het benoemen van tests en testklassen.

Afzonderlijke eenheidstests van integratietests in verschillende projecten. Het scheiden van de tests:

  • Helpt ervoor te zorgen dat onderdelen voor infrastructuurtests niet per ongeluk worden opgenomen in de eenheidstests.
  • Hiermee kunt u bepalen welke set tests worden uitgevoerd.

Er is vrijwel geen verschil tussen de configuratie voor tests van Razor Pagina-apps en MVC-apps. Het enige verschil is in de naam van de tests. In een Razor Pagina-app worden tests van pagina-eindpunten meestal vernoemd naar de paginamodelklasse (bijvoorbeeld IndexPageTests om de integratie van onderdelen voor de indexpagina te testen). In een MVC-app worden tests meestal ingedeeld op controllerklassen en benoemd naar de controllers die ze testen (bijvoorbeeld HomeControllerTests om de integratie van onderdelen voor de Home controller te testen).

Vereisten voor de app testen

Het testproject moet:

Deze vereisten kunnen worden weergegeven in de voorbeeld-app. Inspecteer het tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj bestand. De voorbeeld-app maakt gebruik van het xUnit-testframework en de AngleSharp-parserbibliotheek , dus de voorbeeld-app verwijst ook naar:

In apps die versie 2.4.2 of hoger gebruiken xunit.runner.visualstudio , moet het testproject verwijzen naar het Microsoft.NET.Test.Sdk pakket.

Entity Framework Core wordt ook gebruikt in de tests. Bekijk het projectbestand in GitHub.

SUT-omgeving

Als de omgeving van de SUT niet is ingesteld, wordt de omgeving standaard ingesteld op Ontwikkeling.

Basistests met de standaard WebApplicationFactory

Maak de impliciet gedefinieerde Program klasse beschikbaar voor het testproject door een van de volgende handelingen uit te voeren:

  • Interne typen van de web-app beschikbaar maken voor het testproject. Dit kan worden gedaan in het bestand van het SUT-project (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Maak de Program klasse openbaar met behulp van een gedeeltelijke klassedeclaratie :

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

    De voorbeeld-app maakt gebruik van de Program benadering van gedeeltelijke klassen.

WebApplicationFactory<TEntryPoint> wordt gebruikt om een TestServer voor de integratietests te maken. TEntryPoint is de ingangspuntklasse van de SUT, meestal Program.cs.

Testklassen implementeren een interface voor klassearmaturen (IClassFixture) om aan te geven dat de klasse tests bevat en gedeelde objectexemplaren biedt voor de tests in de klasse.

De volgende testklasse, BasicTests, gebruikt de WebApplicationFactory om de SUT te bootstrapen en verstrekt een HttpClient aan een testmethode, Get_EndpointsReturnSuccessAndCorrectContentType. De methode controleert of de antwoordstatuscode succesvol is (200-299) en of de Content-Type header text/html; charset=utf-8 is voor verschillende app-pagina's.

CreateClient() maakt een instantie van HttpClient dat automatisch omleidingen volgt en cookies verwerkt.

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

Standaard worden niet-essentiële cookies niet bewaard voor aanvragen wanneer het toestemmingsbeleid voor algemene gegevensbescherming is ingeschakeld. Als u niet-essentiële cookies wilt behouden, zoals cookies die door de TempData-provider worden gebruikt, markeert u deze als essentieel in uw tests. Zie cookie voor instructies over het markeren van een als essentieel onderdeel.

AngleSharp versus Application Parts voor antiforgery-controlesystemen

In dit artikel wordt de AngleSharp-parser gebruikt om de antivervalsingcontroles af te handelen door pagina's te laden en de HTML te parseren. Voor het testen van de eindpunten van controller- en Razor paginaweergaven op een lager niveau, zonder te zorgen over hoe ze in de browser worden weergegeven, kunt u overwegen om te gebruiken Application Parts. De benadering toepassingsonderdelen injecteert een controller of Razor pagina in de app die kan worden gebruikt om JSON-aanvragen te maken om de vereiste waarden op te halen. Zie de blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts and associated GitHub repo by Martin Costello (Engelstalig) voor meer informatie.

WebApplicationFactory aanpassen

De configuratie van de webhost kan onafhankelijk van de testklassen worden gemaakt door te erven van WebApplicationFactory<TEntryPoint> om een of meer aangepaste fabrieken te creëren.

  1. Overnemen van WebApplicationFactory en overschrijven ConfigureWebHost. Met IWebHostBuilder kan de serviceverzameling worden geconfigureerd met IWebHostBuilder.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");
        }
    }
    

    Database-seeding in de voorbeeld-app wordt uitgevoerd door de InitializeDbForTests methode. De methode wordt beschreven in het voorbeeld van integratietests: sectie App-organisatie testen .

    De databasecontext van de SUT is geregistreerd in Program.cs. De callback van de test-app builder.ConfigureServices wordt uitgevoerd nadat de code van de app Program.cs is uitgevoerd. Als u een andere database wilt gebruiken voor de tests dan de database van de app, moet de databasecontext van de app worden vervangen in builder.ConfigureServices.

    De voorbeeld-app vindt de servicedescriptor voor de databasecontext en gebruikt de descriptor om de serviceregistratie te verwijderen. De factory voegt vervolgens een nieuwe ApplicationDbContext toe die gebruikmaakt van een in-memory database voor de tests.

    Als u verbinding wilt maken met een andere database, wijzigt u de DbConnection. Een SQL Server-testdatabase gebruiken:

  1. Gebruik de aangepaste CustomWebApplicationFactory in testklassen. In het volgende voorbeeld wordt de factory in de IndexPageTests klasse gebruikt:

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

    De client van de voorbeeld-app is zo geconfigureerd dat het HttpClient geen omleidingen volgt. Zoals verderop in de sectie Mock-verificatie wordt uitgelegd, kunnen tests het resultaat van het eerste antwoord van de app controleren. De eerste reactie is een doorverwijzing in veel van deze tests met een Location header.

  2. Een typische test maakt gebruik van de HttpClient en helpermethoden om de aanvraag en het antwoord te verwerken:

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

Elke POST-aanvraag voor de SUT moet voldoen aan de antivervalsingscontrole die automatisch wordt uitgevoerd door het antivervalsingssysteem voor gegevensbeveiliging van de app. Als u de POST-aanvraag van een test wilt regelen, moet de test-app het volgende doen:

  1. Maak een aanvraag voor de pagina.
  2. Parse het antiforgery-token en het aanvraagvalidatietoken uit cookie het antwoord.
  3. Voer de POST-aanvraag uit met de antiforgery- en aanvraagvalidatietokens cookie.

De SendAsync helper-extensiemethoden () en de Helpers/HttpClientExtensions.cs helpermethode (GetDocumentAsyncHelpers/HtmlHelpers.cs) in de voorbeeld-app gebruiken de AngleSharp-parser om de antivervalsingscontrole af te handelen met de volgende methoden:

  • GetDocumentAsync: ontvangt de HttpResponseMessage en retourneert een IHtmlDocument. GetDocumentAsync maakt gebruik van een fabriek die een virtueel antwoord voorbereidt op basis van het oorspronkelijke HttpResponseMessageantwoord. Zie de documentatie van AngleSharp voor meer informatie.
  • SendAsync extensiemethoden voor het HttpClient opstellen van een HttpRequestMessage en aanroep SendAsync(HttpRequestMessage) om aanvragen naar de SUT te verzenden. Overbelastingen voor SendAsync het accepteren van het HTML-formulier (IHtmlFormElement) en het volgende:
    • Knop Verzenden van het formulier (IHtmlElement)
    • Formulierwaardenverzameling (IEnumerable<KeyValuePair<string, string>>)
    • Verzendknop (IHtmlElement) en formulierwaarden (IEnumerable<KeyValuePair<string, string>>)

AngleSharp is een parseringsbibliotheek van derden die wordt gebruikt voor demonstratiedoeleinden in dit artikel en de voorbeeld-app. AngleSharp wordt niet ondersteund of vereist voor integratietests van ASP.NET Core-apps. Andere parsers kunnen worden gebruikt, zoals het HTML Agility Pack (HAP). Een andere methode is het schrijven van code voor de directe verwerking van het verificatietoken voor aanvragen van het antivervalsingssysteem en cookie. Zie in dit artikel AngleSharp versus Application Parts antiforgery-controles voor meer informatie.

De EF-Core in-memory databaseprovider kan worden gebruikt voor beperkte en eenvoudige tests, maar de SQLite-provider is de aanbevolen keuze voor in-memory tests.

Zie Opstarten uitbreiden met opstartfilters die laten zien hoe u middleware configureert met behulp van IStartupFilter, wat handig is wanneer een test een aangepaste service of middleware vereist.

De client aanpassen met WithWebHostBuilder

Wanneer er extra configuratie is vereist binnen een testmethode, creëert WithWebHostBuilder een nieuwe WebApplicationFactory met een IWebHostBuilder die verder wordt aangepast door middel van configuratie.

De voorbeeldcode roept WithWebHostBuilder om ingerichte services te vervangen door test-stubs. Zie Mock-services injecteren in dit artikel voor meer informatie en voorbeeldgebruik.

De Post_DeleteMessageHandler_ReturnsRedirectToRoot testmethode van de voorbeeld-app demonstreert het gebruik van WithWebHostBuilder. Met deze test wordt een record verwijderd in de database door een formulierverzending in de SUT te activeren.

Omdat een andere test in de IndexPageTests klasse een bewerking uitvoert waarmee alle records in de database worden verwijderd en die vóór de Post_DeleteMessageHandler_ReturnsRedirectToRoot methode kunnen worden uitgevoerd, wordt de database opnieuw verzonden in deze testmethode om ervoor te zorgen dat er een record aanwezig is om de SUT te verwijderen. Het selecteren van de eerste verwijderknop van het messages formulier in de SUT wordt gesimuleerd in de aanvraag naar de SUT:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}

Clientopties

Zie de WebApplicationFactoryClientOptions pagina voor standaardinstellingen en beschikbare opties bij het maken van HttpClient exemplaren.

Maak de WebApplicationFactoryClientOptions klasse en geef deze door aan de 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
        });
    }
}

NOTITIE: Als u HTTPS-omleidingswaarschuwingen in logboeken wilt voorkomen bij het gebruik van HTTPS Redirection Middleware, stelt u BaseAddress = new Uri("https://localhost")

Mock-services injecteren

Services kunnen in een test worden overschreven met een aanroep naar ConfigureTestServices op de host builder. Als u het bereik van de overschreven services voor de test zelf wilt instellen, wordt de WithWebHostBuilder methode gebruikt om een hostbouwer op te halen. Dit is te zien in de volgende tests:

De voorbeeld-SUT bevat een scoped service die een offerte retourneert. Het citaat wordt ingesloten in een verborgen veld op de indexpagina wanneer de indexpagina wordt aangevraagd.

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

De volgende markeringen worden gegenereerd wanneer de SUT-app wordt uitgevoerd:

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

Om de service en offerteinjectie in een integratietest te testen, wordt door de test een mockservice in de SUT geïnjecteerd. De mockservice vervangt de QuoteService van de app door een service die wordt geleverd door de test-app, genaamd TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}

ConfigureTestServices wordt aangeroepen en de scoped service wordt geregistreerd:

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

De opmaak die tijdens de uitvoering van de test wordt geproduceerd, weerspiegelt de citaattekst die is opgegeven door TestQuoteService, waardoor de assertie slaagt.

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

Simulatie-authenticatie

Tests in de AuthTests-klasse controleren of een beveiligd eindpunt:

  • Hiermee wordt een niet-geverifieerde gebruiker omgeleid naar de aanmeldingspagina van de app.
  • Retourneert inhoud voor een geverifieerde gebruiker.

In de SUT gebruikt de /SecurePage pagina een AuthorizePage conventie om een AuthorizeFilter op de pagina toe te passen. Zie Razor Autorisatieconventies voor pagina's voor meer informatie.

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

In de Get_SecurePageRedirectsAnUnauthenticatedUser test wordt een WebApplicationFactoryClientOptions ingesteld om omleidingen te verbieden door AllowAutoRedirect op false in te stellen.

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

Door te voorkomen dat de client de omleiding volgt, kunnen de volgende controles worden uitgevoerd:

  • De statuscode die door de SUT wordt geretourneerd, kan worden gecontroleerd op basis van het verwachte HttpStatusCode.Redirect resultaat, niet de uiteindelijke statuscode na de omleiding naar de aanmeldingspagina, wat zou zijn HttpStatusCode.OK.
  • De Location headerwaarde in de antwoordheaders wordt gecontroleerd om te bevestigen dat deze begint met http://localhost/Identity/Account/Login, niet het laatste antwoord op de aanmeldingspagina, waar de Location header niet aanwezig zou zijn.

De testapp kan een AuthenticationHandler<TOptions> in ConfigureTestServices simuleren om aspecten van authenticatie en autorisatie te testen. Een minimaal scenario retourneert een AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

De TestAuthHandler wordt aangeroepen om een gebruiker te authenticeren wanneer het verificatieschema is ingesteld op TestScheme waar AddAuthentication geregistreerd is voor ConfigureTestServices. Het is belangrijk dat het TestScheme schema overeenkomt met het schema dat uw app verwacht. Anders werkt verificatie niet.

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

Zie de sectie WebApplicationFactoryClientOptions voor meer informatie.

Basistests voor verificatie-middleware

Bekijk deze GitHub-opslagplaats voor basistests van verificatie-middleware. Het bevat een testserver die specifiek is voor het testscenario.

De omgeving instellen

Stel de omgeving in de aangepaste applicatiefabriek in.

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

Hoe de testinfrastructuur het app-inhoudsbasispad afleidt

De WebApplicationFactory constructor bepaalt het hoofdpad van de app-inhoud door te zoeken naar een WebApplicationFactoryContentRootAttribute op de assembly met de integratietests met een sleutel die gelijk is aan de TEntryPoint assembly System.Reflection.Assembly.FullName. Als een kenmerk met de juiste sleutel niet wordt gevonden, wordt overgeschakeld naar het zoeken naar een solutionbestand (WebApplicationFactory) en wordt de assemblynaam toegevoegd aan de solution directory. De hoofdmap van de app (het hoofdpad van de inhoud) wordt gebruikt om weergaven en inhoudsbestanden te detecteren.

Schaduwkopie uitschakelen

Schaduwkopie zorgt ervoor dat de tests worden uitgevoerd in een andere map dan de uitvoermap. Als uw tests afhankelijk zijn van het laden van bestanden ten opzichte Assembly.Location van en u problemen ondervindt, moet u mogelijk schaduwkopie uitschakelen.

Als u schaduwkopie wilt uitschakelen wanneer u xUnit gebruikt, maakt u een xunit.runner.json bestand in uw testprojectmap met de juiste configuratie-instelling:

{
  "shadowCopy": false
}

Verwijdering van objecten

Nadat de tests van de IClassFixture-implementatie zijn uitgevoerd, worden TestServer en HttpClient verwijderd wanneer xUnit de WebApplicationFactory verwijdert. Als objecten die door de ontwikkelaar zijn geïnstantieerd, verwijdering vereisen, moet u deze verwijderen in de IClassFixture implementatie. Zie Een verwijderingsmethode implementeren voor meer informatie.

Voorbeeld van integratietests

De voorbeeld-app bestaat uit twee apps:

Applicatie Projectmap Beschrijving
Bericht-app (de SUT) src/RazorPagesProject Hiermee kan een gebruiker berichten toevoegen, verwijderen, alles verwijderen en analyseren.
App testen tests/RazorPagesProject.Tests Wordt gebruikt om de integratietest van de SUT uit te voeren.

De tests kunnen worden uitgevoerd met behulp van de ingebouwde testfuncties van een IDE, zoals Visual Studio. Als u Visual Studio Code of de opdrachtregel gebruikt, voert u de volgende opdracht uit bij een opdrachtprompt in de tests/RazorPagesProject.Tests map:

dotnet test

Organisatie van de berichtenapp (SUT)

De SUT is een Razor paginaberichtsysteem met de volgende kenmerken:

  • De indexpagina van de app (Pages/Index.cshtml en Pages/Index.cshtml.cs) biedt een ui- en paginamodelmethode om de toevoeging, verwijdering en analyse van berichten (gemiddelde woorden per bericht) te beheren.
  • Een bericht wordt beschreven door de Message klasse (Data/Message.cs) met twee eigenschappen: Id (sleutel) en Text (bericht). De Text eigenschap is vereist en beperkt tot 200 tekens.
  • Berichten worden opgeslagen met behulp van de in-memory database van Entity Framework†.
  • De app bevat een DATA Access Layer (DAL) in de databasecontextklasse (AppDbContextData/AppDbContext.cs).
  • Als de database leeg is bij het opstarten van de app, wordt het berichtenarchief geïnitialiseerd met drie berichten.
  • De app bevat een /SecurePage die alleen toegankelijk is voor een geauthenticeerde gebruiker.

†De EF-artikel, Testen met InMemory, legt uit hoe u een in-memory database gebruikt voor tests met MSTest. In dit onderwerp wordt het xUnit-testframework gebruikt. Testconcepten en test-implementaties in verschillende testframeworks zijn vergelijkbaar, maar niet identiek.

Hoewel de app geen gebruik maakt van het opslagplaatspatroon en geen effectief voorbeeld is van het UoW-patroon (Unit of Work),Razor ondersteunt Pages deze patronen van ontwikkeling. Zie Voor meer informatie het ontwerpen van de infrastructuurpersistentielaag en testcontrollerlogica (het voorbeeld implementeert het opslagplaatspatroon).

App-organisatie testen

De test-app is een console-app in de tests/RazorPagesProject.Tests map.

App-map testen Beschrijving
AuthTests Bevat testmethoden voor:
  • Toegang tot een beveiligde pagina door een niet-geverifieerde gebruiker.
  • Toegang tot een beveiligde pagina door een geverifieerde gebruiker met een mock AuthenticationHandler<TOptions>.
  • Het verkrijgen van een GitHub-gebruikersprofiel en het controleren van de gebruikersaanmelding van het profiel.
BasicTests Bevat een testmethode voor routering en inhoudstype.
IntegrationTests Bevat de integratietests voor de pagina Index met behulp van aangepaste WebApplicationFactory klasse.
Helpers/Utilities
  • Utilities.cs bevat de InitializeDbForTests methode die wordt gebruikt om de database te seeden met testgegevens.
  • HtmlHelpers.cs biedt een methode voor het retourneren van een AngleSharp IHtmlDocument voor gebruik in de testmethoden.
  • HttpClientExtensions.cs biedt overbelastingen voor SendAsync het indienen van aanvragen bij de SUT.

Het testframework is xUnit. Integratietests worden uitgevoerd met de Microsoft.AspNetCore.TestHost, die de TestServer omvat. Omdat het Microsoft.AspNetCore.Mvc.Testing pakket wordt gebruikt voor het configureren van de testhost en testserver, vereisen de pakketten TestHost en TestServer geen directe pakketverwijzingen in het projectbestand van de test-app of in de configuratie voor de ontwikkelaar.

Integratietests vereisen meestal een kleine gegevensset in de database voordat de test wordt uitgevoerd. Met een testoproep voor verwijderen van een databaserecord moet de database bijvoorbeeld ten minste één record hebben om de verwijderaanvraag te laten slagen.

De voorbeeld-app vult de database met drie berichten in Utilities.cs die tests kunnen gebruiken wanneer ze worden uitgevoerd.

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

De databasecontext van de SUT is geregistreerd in Program.cs. De callback van de test-app builder.ConfigureServices wordt uitgevoerd nadat de code van de app Program.cs is uitgevoerd. Als u een andere database voor de tests wilt gebruiken, moet de databasecontext van de app worden vervangen in builder.ConfigureServices. Zie de sectie WebApplicationFactory aanpassen voor meer informatie.

Aanvullende bronnen

In dit artikel wordt ervan uitgegaan dat u basiskennis hebt van eenheidstests. Als u niet bekend bent met testconcepten, raadpleegt u het artikel Unit Testing in .NET Core en .NET Standard en de bijbehorende gekoppelde inhoud.

Voorbeeldcode bekijken of downloaden (hoe download je)

De voorbeeld-app is een Razor Pagina-app en gaat uit van basiskennis van Razor Pages. Als u niet bekend bent met Razor Pagina's, raadpleegt u de volgende artikelen:

Voor het testen van SPA's raden we een hulpprogramma aan, zoals Playwright voor .NET, waarmee een browser kan worden geautomatiseerd.

Inleiding tot integratietests

Integratietests evalueren de onderdelen van een app op een breder niveau dan eenheidstests. Eenheidstests worden gebruikt voor het testen van geïsoleerde softwareonderdelen, zoals afzonderlijke klassemethoden. Integratietests bevestigen dat twee of meer app-onderdelen samenwerken om een verwacht resultaat te produceren, mogelijk inclusief elk onderdeel dat nodig is om een aanvraag volledig te verwerken.

Deze bredere tests worden gebruikt om de infrastructuur en het hele framework van de app te testen, vaak inclusief de volgende onderdelen:

  • Gegevensbank
  • Bestandssysteem
  • Netwerkapparaten
  • Pijplijn voor aanvraag-antwoord

Eenheidstests maken gebruik van ge fabriceerde onderdelen, ook wel nep- of mockobjecten genoemd, in plaats van infrastructuuronderdelen.

In tegenstelling tot eenheidstests, integratietests:

  • Gebruik de werkelijke onderdelen die door de app in productie worden gebruikt.
  • Meer code en gegevensverwerking vereisen.
  • Het duurt langer om uit te voeren.

Beperk daarom het gebruik van integratietests tot de belangrijkste infrastructuurscenario's. Als een gedrag kan worden getest met behulp van een eenheidstest of een integratietest, kiest u de eenheidstest.

In discussies over integratietests wordt het geteste project kortom het systeem onder test of 'SUT' genoemd. "SUT" wordt in dit artikel gebruikt om te verwijzen naar de ASP.NET Core-app die wordt getest.

Schrijf geen integratietests voor elke permutatie van gegevens- en bestandstoegang met databases en bestandssystemen. Ongeacht het aantal plaatsen in een app dat communiceert met databases en bestandssystemen, is een gerichte set integratietests voor lezen, schrijven, bijwerken en verwijderen meestal geschikt voor het testen van database- en bestandssysteemonderdelen. Gebruik eenheidstests voor routinetests van methodelogica die met deze onderdelen communiceren. In eenheidstests resulteert het gebruik van infrastructuurvervalsingen of mocks in een snellere testuitvoering.

ASP.NET Kernintegratietests

Integratietests in ASP.NET Core vereisen het volgende:

  • Een testproject wordt gebruikt om de tests te bevatten en uit te voeren. Het testproject heeft een verwijzing naar de SUT.
  • Het testproject maakt een testwebhost voor de SUT en gebruikt een testserverclient voor het afhandelen van aanvragen en antwoorden met de SUT.
  • Een testloper wordt gebruikt om de tests uit te voeren en de testresultaten te rapporteren.

Integratietests volgen een reeks gebeurtenissen die de gebruikelijke teststappen Rangschikken, Act en Assert bevatten:

  1. De webhost van de SUT is geconfigureerd.
  2. Er wordt een testserverclient gemaakt om aanvragen naar de app te verzenden.
  3. De teststap Rangschikken wordt uitgevoerd: De test-app bereidt een aanvraag voor.
  4. De teststap Act wordt uitgevoerd: de client verzendt de aanvraag en ontvangt het antwoord.
  5. De assert-teststap wordt uitgevoerd: het daadwerkelijke antwoord wordt gevalideerd als een geslaagd of mislukt op basis van een verwacht antwoord.
  6. Het proces wordt voortgezet totdat alle tests worden uitgevoerd.
  7. De testresultaten worden gerapporteerd.

Normaal gesproken is de testwebhost anders geconfigureerd dan de normale webhost van de app voor de testuitvoeringen. Een andere database of andere app-instellingen kunnen bijvoorbeeld worden gebruikt voor de tests.

Infrastructuuronderdelen, zoals de testwebhost en de testserver in het geheugen (TestServer), worden geleverd of beheerd door het microsoft.AspNetCore.Mvc.Testing-pakket . Het gebruik van dit pakket stroomlijnt het maken en uitvoeren van tests.

Het Microsoft.AspNetCore.Mvc.Testing pakket verwerkt de volgende taken:

  • Kopieert het afhankelijkhedenbestand (.deps) van de SUT naar de map van bin het testproject.
  • Hiermee stelt u de inhoudshoofdmap in op de hoofdmap van het SUT-project, zodat statische bestanden en pagina's/weergaven worden gevonden wanneer de tests worden uitgevoerd.
  • Biedt de klasse WebApplicationFactory om het bootstrappen van de SUT te stroomlijnen.TestServer

In de documentatie voor eenheidstests wordt beschreven hoe u een testproject en testloper instelt, samen met gedetailleerde instructies voor het uitvoeren van tests en aanbevelingen voor het benoemen van tests en testklassen.

Afzonderlijke eenheidstests van integratietests in verschillende projecten. Het scheiden van de tests:

  • Helpt ervoor te zorgen dat onderdelen voor infrastructuurtests niet per ongeluk worden opgenomen in de eenheidstests.
  • Hiermee kunt u bepalen welke set tests worden uitgevoerd.

Er is vrijwel geen verschil tussen de configuratie voor tests van Razor Pagina-apps en MVC-apps. Het enige verschil is in de naam van de tests. In een Razor Pagina-app worden tests van pagina-eindpunten meestal vernoemd naar de paginamodelklasse (bijvoorbeeld IndexPageTests om de integratie van onderdelen voor de indexpagina te testen). In een MVC-app worden tests meestal ingedeeld op controllerklassen en benoemd naar de controllers die ze testen (bijvoorbeeld HomeControllerTests om de integratie van onderdelen voor de Home controller te testen).

Vereisten voor de app testen

Het testproject moet:

Deze vereisten kunnen worden weergegeven in de voorbeeld-app. Inspecteer het tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj bestand. De voorbeeld-app maakt gebruik van het xUnit-testframework en de AngleSharp-parserbibliotheek , dus de voorbeeld-app verwijst ook naar:

In apps die versie 2.4.2 of hoger gebruiken xunit.runner.visualstudio , moet het testproject verwijzen naar het Microsoft.NET.Test.Sdk pakket.

Entity Framework Core wordt ook gebruikt in de tests. Bekijk het projectbestand in GitHub.

SUT-omgeving

Als de omgeving van de SUT niet is ingesteld, wordt de omgeving standaard ingesteld op Ontwikkeling.

Basistests met de standaard WebApplicationFactory

Maak de impliciet gedefinieerde Program klasse beschikbaar voor het testproject door een van de volgende handelingen uit te voeren:

  • Interne typen van de web-app beschikbaar maken voor het testproject. Dit kan worden gedaan in het bestand van het SUT-project (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Maak de Program klasse openbaar met behulp van een gedeeltelijke klassedeclaratie :

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

    De voorbeeld-app maakt gebruik van de Program benadering van gedeeltelijke klassen.

WebApplicationFactory<TEntryPoint> wordt gebruikt om een TestServer voor de integratietests te maken. TEntryPoint is de ingangspuntklasse van de SUT, meestal Program.cs.

Testklassen implementeren een interface voor klassearmaturen (IClassFixture) om aan te geven dat de klasse tests bevat en gedeelde objectexemplaren biedt voor de tests in de klasse.

De volgende testklasse, BasicTests, gebruikt de WebApplicationFactory om de SUT te bootstrapen en verstrekt een HttpClient aan een testmethode, Get_EndpointsReturnSuccessAndCorrectContentType. De methode controleert of de antwoordstatuscode succesvol is (200-299) en of de Content-Type header text/html; charset=utf-8 is voor verschillende app-pagina's.

CreateClient() maakt een instantie van HttpClient dat automatisch omleidingen volgt en cookies verwerkt.

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());
    }
}
[TestClass]
public class BasicTests
{
    private static CustomWebApplicationFactory<Program> _factory;

    [ClassInitialize]
    public static void AssemblyInitialize(TestContext _)
    {
        _factory = new CustomWebApplicationFactory<Program>();
    }

    [ClassCleanup(ClassCleanupBehavior.EndOfClass)]
    public static void AssemblyCleanup(TestContext _)
    {
        _factory.Dispose();
    }

    [TestMethod]
    [DataRow("/")]
    [DataRow("/Index")]
    [DataRow("/About")]
    [DataRow("/Privacy")]
    [DataRow("/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.AreEqual("text/html; charset=utf-8",
            response.Content.Headers.ContentType.ToString());
    }
}
public class BasicTests
{
    private CustomWebApplicationFactory<Program>
        _factory;

    [SetUp]
    public void SetUp()
    {
        _factory = new CustomWebApplicationFactory<Program>();
    }

    [TearDown]
    public void TearDown()
    {
        _factory.Dispose();
    }

    [DatapointSource]
    public string[] values = ["/", "/Index", "/About", "/Privacy", "/Contact"];

    [Theory]
    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.That(response.Content.Headers.ContentType.ToString(), Is.EqualTo("text/html; charset=utf-8"));
    }
}

Standaard worden niet-essentiële cookies niet bewaard voor aanvragen wanneer het toestemmingsbeleid voor algemene gegevensbescherming is ingeschakeld. Als u niet-essentiële cookies wilt behouden, zoals cookies die door de TempData-provider worden gebruikt, markeert u deze als essentieel in uw tests. Zie cookie voor instructies over het markeren van een als essentieel onderdeel.

AngleSharp versus Application Parts voor antiforgery-controlesystemen

In dit artikel wordt de AngleSharp-parser gebruikt om de antivervalsingcontroles af te handelen door pagina's te laden en de HTML te parseren. Voor het testen van de eindpunten van controller- en Razor paginaweergaven op een lager niveau, zonder te zorgen over hoe ze in de browser worden weergegeven, kunt u overwegen om te gebruiken Application Parts. De benadering toepassingsonderdelen injecteert een controller of Razor pagina in de app die kan worden gebruikt om JSON-aanvragen te maken om de vereiste waarden op te halen. Zie de blog Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts and associated GitHub repo by Martin Costello (Engelstalig) voor meer informatie.

WebApplicationFactory aanpassen

De configuratie van de webhost kan onafhankelijk van de testklassen worden gemaakt door te erven van WebApplicationFactory<TEntryPoint> om een of meer aangepaste fabrieken te creëren.

  1. Overnemen van WebApplicationFactory en overschrijven ConfigureWebHost. Met IWebHostBuilder kan de serviceverzameling worden geconfigureerd met IWebHostBuilder.ConfigureServices

    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType == 
                        typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    
    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    
    public class CustomWebApplicationFactory<TProgram>
        : WebApplicationFactory<TProgram> where TProgram : class
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.ConfigureServices(services =>
            {
                var dbContextDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));
    
                services.Remove(dbContextDescriptor);
    
                var dbConnectionDescriptor = services.SingleOrDefault(
                    d => d.ServiceType ==
                        typeof(DbConnection));
    
                services.Remove(dbConnectionDescriptor);
    
                // Create open SqliteConnection so EF won't automatically close it.
                services.AddSingleton<DbConnection>(container =>
                {
                    var connection = new SqliteConnection("DataSource=:memory:");
                    connection.Open();
    
                    return connection;
                });
    
                services.AddDbContext<ApplicationDbContext>((container, options) =>
                {
                    var connection = container.GetRequiredService<DbConnection>();
                    options.UseSqlite(connection);
                });
            });
    
            builder.UseEnvironment("Development");
        }
    }
    

    Database-seeding in de voorbeeld-app wordt uitgevoerd door de InitializeDbForTests methode. De methode wordt beschreven in het voorbeeld van integratietests: sectie App-organisatie testen .

    De databasecontext van de SUT is geregistreerd in Program.cs. De callback van de test-app builder.ConfigureServices wordt uitgevoerd nadat de code van de app Program.cs is uitgevoerd. Als u een andere database wilt gebruiken voor de tests dan de database van de app, moet de databasecontext van de app worden vervangen in builder.ConfigureServices.

    De voorbeeld-app vindt de servicedescriptor voor de databasecontext en gebruikt de descriptor om de serviceregistratie te verwijderen. De factory voegt vervolgens een nieuwe ApplicationDbContext toe die gebruikmaakt van een in-memory database voor de tests.

    Als u verbinding wilt maken met een andere database, wijzigt u de DbConnection. Een SQL Server-testdatabase gebruiken:

  1. Gebruik de aangepaste CustomWebApplicationFactory in testklassen. In het volgende voorbeeld wordt de factory in de IndexPageTests klasse gebruikt:

    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
            });
        }
    
    [TestClass]
    public class IndexPageTests
    {
        private static HttpClient _client;
        private static CustomWebApplicationFactory<Program>
            _factory;
    
        [ClassInitialize]
        public static void AssemblyInitialize(TestContext _)
        {
            _factory = new CustomWebApplicationFactory<Program>();
            _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
        }
    
        [ClassCleanup(ClassCleanupBehavior.EndOfClass)]
        public static void AssemblyCleanup(TestContext _)
        {
            _factory.Dispose();
        }
    
    public class IndexPageTests
    {
    
        private HttpClient _client;
        private CustomWebApplicationFactory<Program>
            _factory;
    
        [SetUp]
        public void SetUp()
        {
            _factory = new CustomWebApplicationFactory<Program>();
            _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
            {
                AllowAutoRedirect = false
            });
        }
    
        [TearDown]
        public void TearDown()
        {
            _factory.Dispose();
            _client.Dispose();
        }
    

    De client van de voorbeeld-app is zo geconfigureerd dat het HttpClient geen omleidingen volgt. Zoals verderop in de sectie Mock-verificatie wordt uitgelegd, kunnen tests het resultaat van het eerste antwoord van de app controleren. De eerste reactie is een doorverwijzing in veel van deze tests met een Location header.

  2. Een typische test maakt gebruik van de HttpClient en helpermethoden om de aanvraag en het antwoord te verwerken:

    [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);
    }
    
    [TestMethod]
    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.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
        Assert.AreEqual("/", response.Headers.Location.OriginalString);
    }
    
    [Test]
    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.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK));
        Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
        Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/"));
    }
    

Elke POST-aanvraag voor de SUT moet voldoen aan de antivervalsingscontrole die automatisch wordt uitgevoerd door het antivervalsingssysteem voor gegevensbeveiliging van de app. Als u de POST-aanvraag van een test wilt regelen, moet de test-app het volgende doen:

  1. Maak een aanvraag voor de pagina.
  2. Parse het antiforgery-token en het aanvraagvalidatietoken uit cookie het antwoord.
  3. Voer de POST-aanvraag uit met de antiforgery- en aanvraagvalidatietokens cookie.

De SendAsync helper-extensiemethoden () en de Helpers/HttpClientExtensions.cs helpermethode (GetDocumentAsyncHelpers/HtmlHelpers.cs) in de voorbeeld-app gebruiken de AngleSharp-parser om de antivervalsingscontrole af te handelen met de volgende methoden:

  • GetDocumentAsync: ontvangt de HttpResponseMessage en retourneert een IHtmlDocument. GetDocumentAsync maakt gebruik van een fabriek die een virtueel antwoord voorbereidt op basis van het oorspronkelijke HttpResponseMessageantwoord. Zie de documentatie van AngleSharp voor meer informatie.
  • SendAsync extensiemethoden voor het HttpClient opstellen van een HttpRequestMessage en aanroep SendAsync(HttpRequestMessage) om aanvragen naar de SUT te verzenden. Overbelastingen voor SendAsync het accepteren van het HTML-formulier (IHtmlFormElement) en het volgende:
    • Knop Verzenden van het formulier (IHtmlElement)
    • Formulierwaardenverzameling (IEnumerable<KeyValuePair<string, string>>)
    • Verzendknop (IHtmlElement) en formulierwaarden (IEnumerable<KeyValuePair<string, string>>)

AngleSharp is een parseringsbibliotheek van derden die wordt gebruikt voor demonstratiedoeleinden in dit artikel en de voorbeeld-app. AngleSharp wordt niet ondersteund of vereist voor integratietests van ASP.NET Core-apps. Andere parsers kunnen worden gebruikt, zoals het HTML Agility Pack (HAP). Een andere methode is het schrijven van code voor de directe verwerking van het verificatietoken voor aanvragen van het antivervalsingssysteem en cookie. Zie in dit artikel AngleSharp versus Application Parts antiforgery-controles voor meer informatie.

De EF-Core in-memory databaseprovider kan worden gebruikt voor beperkte en eenvoudige tests, maar de SQLite-provider is de aanbevolen keuze voor in-memory tests.

Zie Opstarten uitbreiden met opstartfilters die laten zien hoe u middleware configureert met behulp van IStartupFilter, wat handig is wanneer een test een aangepaste service of middleware vereist.

De client aanpassen met WithWebHostBuilder

Wanneer er extra configuratie is vereist binnen een testmethode, creëert WithWebHostBuilder een nieuwe WebApplicationFactory met een IWebHostBuilder die verder wordt aangepast door middel van configuratie.

De voorbeeldcode roept WithWebHostBuilder om ingerichte services te vervangen door test-stubs. Zie Mock-services injecteren in dit artikel voor meer informatie en voorbeeldgebruik.

De Post_DeleteMessageHandler_ReturnsRedirectToRoot testmethode van de voorbeeld-app demonstreert het gebruik van WithWebHostBuilder. Met deze test wordt een record verwijderd in de database door een formulierverzending in de SUT te activeren.

Omdat een andere test in de IndexPageTests klasse een bewerking uitvoert waarmee alle records in de database worden verwijderd en die vóór de Post_DeleteMessageHandler_ReturnsRedirectToRoot methode kunnen worden uitgevoerd, wordt de database opnieuw verzonden in deze testmethode om ervoor te zorgen dat er een record aanwezig is om de SUT te verwijderen. Het selecteren van de eerste verwijderknop van het messages formulier in de SUT wordt gesimuleerd in de aanvraag naar de SUT:

[Fact]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

    var defaultPage = await _client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);

    //Act
    var response = await _client.SendAsync(
        (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
        (IHtmlButtonElement)content.QuerySelector("form[id='messages']")
            .QuerySelector("div[class='panel-body']")
            .QuerySelector("button"));

    // Assert
    Assert.Equal(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.Equal("/", response.Headers.Location.OriginalString);
}
[TestMethod]
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.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
    Assert.AreEqual("/", response.Headers.Location.OriginalString);
}
[Test]
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.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK));
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
    Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/"));
}

Clientopties

Zie de WebApplicationFactoryClientOptions pagina voor standaardinstellingen en beschikbare opties bij het maken van HttpClient exemplaren.

Maak de WebApplicationFactoryClientOptions klasse en geef deze door aan de 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
        });
    }
[TestClass]
public class IndexPageTests
{
    private static HttpClient _client;
    private static CustomWebApplicationFactory<Program>
        _factory;

    [ClassInitialize]
    public static void AssemblyInitialize(TestContext _)
    {
        _factory = new CustomWebApplicationFactory<Program>();
        _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

    [ClassCleanup(ClassCleanupBehavior.EndOfClass)]
    public static void AssemblyCleanup(TestContext _)
    {
        _factory.Dispose();
    }
public class IndexPageTests
{

    private HttpClient _client;
    private CustomWebApplicationFactory<Program>
        _factory;

    [SetUp]
    public void SetUp()
    {
        _factory = new CustomWebApplicationFactory<Program>();
        _client = _factory.CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });
    }

    [TearDown]
    public void TearDown()
    {
        _factory.Dispose();
        _client.Dispose();
    }

NOTITIE: Als u HTTPS-omleidingswaarschuwingen in logboeken wilt voorkomen bij het gebruik van HTTPS Redirection Middleware, stelt u BaseAddress = new Uri("https://localhost")

Mock-services injecteren

Services kunnen in een test worden overschreven met een aanroep naar ConfigureTestServices op de host builder. Als u het bereik van de overschreven services voor de test zelf wilt instellen, wordt de WithWebHostBuilder methode gebruikt om een hostbouwer op te halen. Dit is te zien in de volgende tests:

De voorbeeld-SUT bevat een scoped service die een offerte retourneert. Het citaat wordt ingesloten in een verborgen veld op de indexpagina wanneer de indexpagina wordt aangevraagd.

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

De volgende markeringen worden gegenereerd wanneer de SUT-app wordt uitgevoerd:

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

Om de service en offerteinjectie in een integratietest te testen, wordt door de test een mockservice in de SUT geïnjecteerd. De mockservice vervangt de QuoteService van de app door een service die wordt geleverd door de test-app, genaamd TestQuoteService:

IntegrationTests.IndexPageTests.cs:

// Quote ©1975 BBC: The Doctor (Tom Baker); Pyramids of Mars
// https://www.bbc.co.uk/programmes/p00pys55
public class TestQuoteService : IQuoteService
{
    public Task<string> GenerateQuote()
    {
        return Task.FromResult(
            "Something's interfering with time, Mr. Scarman, " +
            "and time is my business.");
    }
}
// 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.");
    }
}
// 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 wordt aangeroepen en de scoped service wordt geregistreerd:

[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);
}
[TestMethod]
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.AreEqual("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}
[Test]
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.That(quoteElement.Attributes["value"].Value, Is.EqualTo(
        "Something's interfering with time, Mr. Scarman, " +
        "and time is my business."));
}

De opmaak die tijdens de uitvoering van de test wordt geproduceerd, weerspiegelt de citaattekst die is opgegeven door TestQuoteService, waardoor de assertie slaagt.

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

Simulatie-authenticatie

Tests in de AuthTests-klasse controleren of een beveiligd eindpunt:

  • Hiermee wordt een niet-geverifieerde gebruiker omgeleid naar de aanmeldingspagina van de app.
  • Retourneert inhoud voor een geverifieerde gebruiker.

In de SUT gebruikt de /SecurePage pagina een AuthorizePage conventie om een AuthorizeFilter op de pagina toe te passen. Zie Razor Autorisatieconventies voor pagina's voor meer informatie.

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

In de Get_SecurePageRedirectsAnUnauthenticatedUser test wordt een WebApplicationFactoryClientOptions ingesteld om omleidingen te verbieden door AllowAutoRedirect op false in te stellen.

[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);
}
[TestMethod]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
    StringAssert.StartsWith(response.Headers.Location.OriginalString, "http://localhost/Identity/Account/Login");
}
[Test]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

    // Act
    var response = await client.GetAsync("/SecurePage");

    // Assert
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
    Assert.That(response.Headers.Location.OriginalString, Does.StartWith("http://localhost/Identity/Account/Login"));
}

Door te voorkomen dat de client de omleiding volgt, kunnen de volgende controles worden uitgevoerd:

  • De statuscode die door de SUT wordt geretourneerd, kan worden gecontroleerd op basis van het verwachte HttpStatusCode.Redirect resultaat, niet de uiteindelijke statuscode na de omleiding naar de aanmeldingspagina, wat zou zijn HttpStatusCode.OK.
  • De Location headerwaarde in de antwoordheaders wordt gecontroleerd om te bevestigen dat deze begint met http://localhost/Identity/Account/Login, niet het laatste antwoord op de aanmeldingspagina, waar de Location header niet aanwezig zou zijn.

De testapp kan een AuthenticationHandler<TOptions> in ConfigureTestServices simuleren om aspecten van authenticatie en autorisatie te testen. Een minimaal scenario retourneert een AuthenticateResult.Success:

public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var claims = new[] { new Claim(ClaimTypes.Name, "Test user") };
        var identity = new ClaimsIdentity(claims, "Test");
        var principal = new ClaimsPrincipal(identity);
        var ticket = new AuthenticationTicket(principal, "TestScheme");

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
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);
    }
}
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);
    }
}

De TestAuthHandler wordt aangeroepen om een gebruiker te authenticeren wanneer het verificatieschema is ingesteld op TestScheme waar AddAuthentication geregistreerd is voor ConfigureTestServices. Het is belangrijk dat het TestScheme schema overeenkomt met het schema dat uw app verwacht. Anders werkt verificatie niet.

[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);
}
[TestMethod]
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.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
[Test]
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.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}

Zie de sectie WebApplicationFactoryClientOptions voor meer informatie.

Basistests voor verificatie-middleware

Bekijk deze GitHub-opslagplaats voor basistests van verificatie-middleware. Het bevat een testserver die specifiek is voor het testscenario.

De omgeving instellen

Stel de omgeving in de aangepaste applicatiefabriek in.

public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType == 
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}
public class CustomWebApplicationFactory<TProgram>
    : WebApplicationFactory<TProgram> where TProgram : class
{
    protected override void ConfigureWebHost(IWebHostBuilder builder)
    {
        builder.ConfigureServices(services =>
        {
            var dbContextDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(IDbContextOptionsConfiguration<ApplicationDbContext>));

            services.Remove(dbContextDescriptor);

            var dbConnectionDescriptor = services.SingleOrDefault(
                d => d.ServiceType ==
                    typeof(DbConnection));

            services.Remove(dbConnectionDescriptor);

            // Create open SqliteConnection so EF won't automatically close it.
            services.AddSingleton<DbConnection>(container =>
            {
                var connection = new SqliteConnection("DataSource=:memory:");
                connection.Open();

                return connection;
            });

            services.AddDbContext<ApplicationDbContext>((container, options) =>
            {
                var connection = container.GetRequiredService<DbConnection>();
                options.UseSqlite(connection);
            });
        });

        builder.UseEnvironment("Development");
    }
}

Hoe de testinfrastructuur het app-inhoudsbasispad afleidt

De WebApplicationFactory constructor bepaalt het hoofdpad van de app-inhoud door te zoeken naar een WebApplicationFactoryContentRootAttribute op de assembly met de integratietests met een sleutel die gelijk is aan de TEntryPoint assembly System.Reflection.Assembly.FullName. Als een kenmerk met de juiste sleutel niet wordt gevonden, wordt overgeschakeld naar het zoeken naar een solutionbestand (WebApplicationFactory) en wordt de assemblynaam toegevoegd aan de solution directory. De hoofdmap van de app (het hoofdpad van de inhoud) wordt gebruikt om weergaven en inhoudsbestanden te detecteren.

Schaduwkopie uitschakelen

Schaduwkopie zorgt ervoor dat de tests worden uitgevoerd in een andere map dan de uitvoermap. Als uw tests afhankelijk zijn van het laden van bestanden ten opzichte Assembly.Location van en u problemen ondervindt, moet u mogelijk schaduwkopie uitschakelen.

Als u schaduwkopie wilt uitschakelen wanneer u xUnit gebruikt, maakt u een xunit.runner.json bestand in uw testprojectmap met de juiste configuratie-instelling:

{
  "shadowCopy": false
}

Verwijdering van objecten

Nadat de tests van de IClassFixture-implementatie zijn uitgevoerd, worden TestServer en HttpClient verwijderd wanneer xUnit de WebApplicationFactory verwijdert. Als objecten die door de ontwikkelaar zijn geïnstantieerd, verwijdering vereisen, moet u deze verwijderen in de IClassFixture implementatie. Zie Een verwijderingsmethode implementeren voor meer informatie.

Nadat de tests van TestClass zijn uitgevoerd, worden TestServer en HttpClient verwijderd wanneer MSTest de WebApplicationFactory in de ClassCleanup-methode verwijdert. Als objecten die door de ontwikkelaar zijn geïnstantieerd, verwijdering vereisen, moet u deze in de ClassCleanup methode verwijderen. Zie Een verwijderingsmethode implementeren voor meer informatie.

Nadat de tests van de testklasse zijn uitgevoerd, worden TestServer en HttpClient verwijderd wanneer NUnit WebApplicationFactory afhandelt tijdens de TearDown methode. Als objecten die door de ontwikkelaar zijn geïnstantieerd, verwijdering vereisen, moet u deze in de TearDown methode verwijderen. Zie Een verwijderingsmethode implementeren voor meer informatie.

Voorbeeld van integratietests

De voorbeeld-app bestaat uit twee apps:

Applicatie Projectmap Beschrijving
Bericht-app (de SUT) src/RazorPagesProject Hiermee kan een gebruiker berichten toevoegen, verwijderen, alles verwijderen en analyseren.
App testen tests/RazorPagesProject.Tests Wordt gebruikt om de integratietest van de SUT uit te voeren.

De tests kunnen worden uitgevoerd met behulp van de ingebouwde testfuncties van een IDE, zoals Visual Studio. Als u Visual Studio Code of de opdrachtregel gebruikt, voert u de volgende opdracht uit bij een opdrachtprompt in de tests/RazorPagesProject.Tests map:

dotnet test

Organisatie van de berichtenapp (SUT)

De SUT is een Razor paginaberichtsysteem met de volgende kenmerken:

  • De indexpagina van de app (Pages/Index.cshtml en Pages/Index.cshtml.cs) biedt een ui- en paginamodelmethode om de toevoeging, verwijdering en analyse van berichten (gemiddelde woorden per bericht) te beheren.
  • Een bericht wordt beschreven door de Message klasse (Data/Message.cs) met twee eigenschappen: Id (sleutel) en Text (bericht). De Text eigenschap is vereist en beperkt tot 200 tekens.
  • Berichten worden opgeslagen met behulp van de in-memory database van Entity Framework†.
  • De app bevat een DATA Access Layer (DAL) in de databasecontextklasse (AppDbContextData/AppDbContext.cs).
  • Als de database leeg is bij het opstarten van de app, wordt het berichtenarchief geïnitialiseerd met drie berichten.
  • De app bevat een /SecurePage die alleen toegankelijk is voor een geauthenticeerde gebruiker.

†De EF-artikel, Testen met InMemory, legt uit hoe u een in-memory database gebruikt voor tests met MSTest. In dit onderwerp wordt het xUnit-testframework gebruikt. Testconcepten en test-implementaties in verschillende testframeworks zijn vergelijkbaar, maar niet identiek.

Hoewel de app geen gebruik maakt van het opslagplaatspatroon en geen effectief voorbeeld is van het UoW-patroon (Unit of Work),Razor ondersteunt Pages deze patronen van ontwikkeling. Zie Voor meer informatie het ontwerpen van de infrastructuurpersistentielaag en testcontrollerlogica (het voorbeeld implementeert het opslagplaatspatroon).

App-organisatie testen

De test-app is een console-app in de tests/RazorPagesProject.Tests map.

App-map testen Beschrijving
AuthTests Bevat testmethoden voor:
  • Toegang tot een beveiligde pagina door een niet-geverifieerde gebruiker.
  • Toegang tot een beveiligde pagina door een geverifieerde gebruiker met een mock AuthenticationHandler<TOptions>.
  • Het verkrijgen van een GitHub-gebruikersprofiel en het controleren van de gebruikersaanmelding van het profiel.
BasicTests Bevat een testmethode voor routering en inhoudstype.
IntegrationTests Bevat de integratietests voor de pagina Index met behulp van aangepaste WebApplicationFactory klasse.
Helpers/Utilities
  • Utilities.cs bevat de InitializeDbForTests methode die wordt gebruikt om de database te seeden met testgegevens.
  • HtmlHelpers.cs biedt een methode voor het retourneren van een AngleSharp IHtmlDocument voor gebruik in de testmethoden.
  • HttpClientExtensions.cs biedt overbelastingen voor SendAsync het indienen van aanvragen bij de SUT.

Het testframework is xUnit. Integratietests worden uitgevoerd met de Microsoft.AspNetCore.TestHost, die de TestServer omvat. Omdat het Microsoft.AspNetCore.Mvc.Testing pakket wordt gebruikt voor het configureren van de testhost en testserver, vereisen de pakketten TestHost en TestServer geen directe pakketverwijzingen in het projectbestand van de test-app of in de configuratie voor de ontwikkelaar.

Integratietests vereisen meestal een kleine gegevensset in de database voordat de test wordt uitgevoerd. Met een testoproep voor verwijderen van een databaserecord moet de database bijvoorbeeld ten minste één record hebben om de verwijderaanvraag te laten slagen.

De voorbeeld-app vult de database met drie berichten in Utilities.cs die tests kunnen gebruiken wanneer ze worden uitgevoerd.

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

De databasecontext van de SUT is geregistreerd in Program.cs. De callback van de test-app builder.ConfigureServices wordt uitgevoerd nadat de code van de app Program.cs is uitgevoerd. Als u een andere database voor de tests wilt gebruiken, moet de databasecontext van de app worden vervangen in builder.ConfigureServices. Zie de sectie WebApplicationFactory aanpassen voor meer informatie.

Aanvullende bronnen