Dela via


Integreringstester i ASP.NET Core

Av Jos van der Til, Martin Costello, och Javier Calvarro Nelson.

Integreringstester säkerställer att en apps komponenter fungerar korrekt på en nivå som innehåller appens stödinfrastruktur, till exempel databasen, filsystemet och nätverket. ASP.NET Core stöder integreringstester med hjälp av ett enhetstestramverk med en testwebbvärd och en minnesintern testserver.

Den här artikeln förutsätter en grundläggande förståelse av enhetstester. Om du inte är bekant med testbegrepp kan du läsa artikeln Enhetstestning i .NET Core och .NET Standard och dess länkade innehåll.

Visa eller ladda ned exempelkod (hur du laddar ned)

Exempelappen är en Razor Pages-app och förutsätter en grundläggande förståelse för Razor Pages. Om du inte är bekant med Razor Pages kan du läsa följande artiklar:

För att testa SPA:errekommenderar vi ett verktyg som Playwright för .NET, som kan automatisera en webbläsare.

Introduktion till integreringstester

Integreringstester utvärderar en apps komponenter på en bredare nivå än enhetstester. Enhetstester används för att testa isolerade programvarukomponenter, till exempel enskilda klassmetoder. Integreringstester bekräftar att två eller flera appkomponenter fungerar tillsammans för att skapa ett förväntat resultat, eventuellt inklusive varje komponent som krävs för att bearbeta en begäran fullt ut.

Dessa bredare tester används för att testa appens infrastruktur och hela ramverket, ofta med följande komponenter:

  • Databas
  • Filsystem
  • Nätverksinstallationer
  • Flöde för begäran-svar

Enhetstester använder fabricerade komponenter, så kallade falska eller falska objekt, i stället för infrastrukturkomponenter.

Till skillnad från enhetstester, integreringstester:

  • Använd de faktiska komponenter som appen använder i produktion.
  • Kräv mer kod och databearbetning.
  • Tar längre tid att genomföra.

Begränsa därför användningen av integreringstester till de viktigaste infrastrukturscenarierna. Om ett beteende kan testas med antingen ett enhetstest eller ett integreringstest väljer du enhetstestet.

I diskussioner om integreringstester kallas det testade projektet ofta System Under Test, eller "SUT" för kort. "SUT" används i hela den här artikeln för att referera till ASP.NET Core-appen som testas.

Skriv inte integreringstester för varje permutation av data och filåtkomst med databaser och filsystem. Oavsett hur många platser i en app som interagerar med databaser och filsystem kan en prioriterad uppsättning av integreringstester för läsning, skrivning, uppdatering och borttagning vanligtvis testa databas- och filsystemkomponenter på ett tillfredsställande sätt. Använd enhetstester för rutinmässiga tester av metodlogik som interagerar med dessa komponenter. I enhetstester resulterar användningen av förfalskningar eller hån i infrastrukturen i snabbare testkörning.

ASP.NET Core-integreringstester

Integreringstester i ASP.NET Core kräver följande:

  • Ett testprojekt används för att innehålla och köra testerna. Testprojektet har en referens till SUT.
  • Testprojektet skapar en testwebbvärd för SUT och använder en testserverklient för att hantera begäranden och svar med SUT.
  • En testkörare används för att köra testerna och rapportera testresultaten.

Integreringstester följer en sekvens av händelser som innehåller de vanliga teststegen Ordna, Agera, och Bekräfta:

  1. SUT:s webbhotell är konfigurerat.
  2. En testserverklient skapas för att skicka begäranden till appen.
  3. Teststeget Ordna körs: Testappen förbereder en begäran.
  4. Teststeget Act körs: Klienten skickar begäran och tar emot svaret.
  5. Teststeget Assert körs: Det faktiska svaret verifieras som antingen godkänd eller underkänd baserat på ett förväntat svar.
  6. Processen fortsätter tills alla tester körs.
  7. Testresultaten rapporteras.

Vanligtvis är testwebbvärden konfigurerad annorlunda än appens normala webbvärd för tester. Till exempel kan en annan databas eller olika appinställningar användas för testerna.

Infrastrukturkomponenter, till exempel testwebbvärden och minnesintern testserver (TestServer), tillhandahålls eller hanteras av Microsoft.AspNetCore.Mvc.Testing-paketet. Användning av det här paketet effektiviserar skapande och körning av test.

Microsoft.AspNetCore.Mvc.Testing-paketet hanterar följande uppgifter:

  • Kopierar beroendefilen (.deps) från SUT till testprojektets bin katalog.
  • Anger innehållsroten till SUT:s projektrot så att statiska filer och sidor/vyer hittas när testerna körs.
  • Tillhandahåller klassen WebApplicationFactory för att effektivisera initieringen av SUT tillsammans med TestServer.

I enhetstester dokumentationen beskrivs hur du konfigurerar ett testprojekt och en testlöpare, tillsammans med detaljerade instruktioner om hur du kör tester och rekommendationer för hur du namnger tester och testklasser.

Separera enhetstester från integreringstester i olika projekt. Separera testerna:

  • Hjälper till att säkerställa att komponenter för infrastrukturtestning inte oavsiktligt ingår i enhetstesterna.
  • Tillåter kontroll över vilken uppsättning tester som körs.

Det finns praktiskt taget ingen skillnad mellan konfigurationen för tester av Razor Pages-appar och MVC-appar. Den enda skillnaden är hur testerna namnges. I en Razor Pages-app namnges vanligtvis tester av sidslutpunkter efter sidmodellklassen (till exempel IndexPageTests för att testa komponentintegrering för indexsidan). I en MVC-app ordnas tester vanligtvis efter kontrollantklasser och namnges efter de kontrollanter som de testar (till exempel HomeControllerTests för att testa komponentintegrering för Home kontrollanten).

Krav för testapp

Testprojektet måste:

Dessa förutsättningar kan ses i exempelappen. Granska filen tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. Exempelappen använder testramverket xUnit och AngleSharp parser-biblioteket, så exempelappen refererar också till:

I appar som använder xunit.runner.visualstudio version 2.4.2 eller senare måste testprojektet referera till Microsoft.NET.Test.Sdk-paketet.

Entity Framework Core används också i testerna. Se -projektfilen i GitHub.

SUT-miljö

Om SUT:s miljö inte har angetts, är standardmiljön utveckling.

Grundläggande tester med standard-WebApplicationFactory

WebApplicationFactory<TEntryPoint> används för att skapa en TestServer för integreringstesterna. TEntryPoint är startpunktsklassen för SUT, vanligtvis Program.cs.

Testklasserna implementerar ett klassfixtur gränssnitt (IClassFixture) för att indikera att klassen innehåller tester och för att tillhandahålla delade objektinstanser för testerna i klassen.

Följande testklass, BasicTests, använder WebApplicationFactory för att initiera SUT och tillhandahålla HttpClient till en testmetod, Get_EndpointsReturnSuccessAndCorrectContentType. Metoden verifierar att svarsstatuskoden är lyckad (200–299) och att Content-Type-huvudet är text/html; charset=utf-8 för flera applikationssidor.

CreateClient() skapar en instans av HttpClient som automatiskt följer omdirigeringar och hanterar cookies.

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

Som standard bevaras inte icke-nödvändiga cookies mellan begäranden när princip för medgivande för den allmänna dataskyddsförordningen är aktiverad. Om du vill bevara icke-nödvändiga cookies, till exempel de som används av TempData-providern, markerar du dem som viktiga i dina tester. Anvisningar om hur du markerar ett cookie som viktigt finns i Viktiga cookies.

AngleSharp jämfört med Application Parts för förfalskningskontroller

Den här artikeln använder AngleSharp parser för att hantera kontroller mot förfalskning genom att läsa in sidor och parsa HTML. Om du vill testa slutpunkterna för kontrollant- och Razor Pages-vyer på en lägre nivå, utan att bry dig om hur de återges i webbläsaren, bör du överväga att använda Application Parts. Metoden Application Parts injicerar en kontroller eller Razor Page i appen som kan användas för att göra JSON-begäranden och hämta de värden som krävs. Mer information finns i bloggen Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts and associated GitHub repo by Martin Costello.

Anpassa WebApplicationFactory

Konfiguration av webbhotell kan skapas oberoende av testklasserna genom att ärva från WebApplicationFactory<TEntryPoint> och skapa en eller flera anpassade fabriker.

  1. Ärv från WebApplicationFactory och åsidosätt ConfigureWebHost. Med IWebHostBuilder kan du konfigurera tjänstsamlingen med 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");
        }
    }
    

    Databasutsöndring i exempelappen utförs av metoden InitializeDbForTests. Metoden beskrivs i exemplen för integrationstester: avsnittet Test för apporganisation.

    SUT:s databaskontext registreras i Program.cs. Testappens builder.ConfigureServices callback-funktion körs efter att appens Program.cs kod har körts. Om du vill använda en annan databas för testerna än appens databas måste appens databaskontext ersättas i builder.ConfigureServices.

    Exempelappen hittar tjänstbeskrivningen för databaskontexten och använder beskrivningen för att ta bort tjänstregistreringen. Fabriken lägger sedan till en ny ApplicationDbContext som använder en minnesintern databas för testerna..

    Om du vill ansluta till en annan databas ändrar du DbConnection. Så här använder du en SQL Server-testdatabas:

  1. Använd den skräddarsydda CustomWebApplicationFactory i testklasser. I följande exempel används fabriken i klassen IndexPageTests:

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

    Exempelappens klient är konfigurerad för att förhindra att HttpClient följer omdirigeringar. Som beskrivs senare i avsnittet Mock authentication tillåter detta tester att kontrollera resultatet av appens första svar. Det första svaret är en omdirigering i många av dessa tester med ett Location-huvud.

  2. Ett typiskt test använder HttpClient- och hjälpmetoderna för att bearbeta begäran och svaret:

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

Alla POST-begäranden till SUT måste uppfylla den antiforgery-kontroll som automatiskt görs av appens dataskyddsskyddsskyddssystem. För att hantera en POST-begäran för ett test måste testappen:

  1. Gör en begäran för sidan.
  2. Parsa antiforgery-cookie och begär valideringstoken från svaret.
  3. Gör POST-begäran med antiforgery-cookie och begär valideringstoken på plats.

SendAsync-tilläggsmetoderna (Helpers/HttpClientExtensions.cs) och GetDocumentAsync-hjälpmetoden (Helpers/HtmlHelpers.cs) i exempelappen använder AngleSharp parser för att hantera antiförfalskningskontroll med följande metoder:

  • GetDocumentAsync: Tar emot HttpResponseMessage och returnerar en IHtmlDocument. GetDocumentAsync använder en fabrik som förbereder ett virtuellt svar baserat på den ursprungliga HttpResponseMessage. Mer information finns i AngleSharp-dokumentationen.
  • SendAsync tilläggsmetoder för HttpClient skapa en HttpRequestMessage och anropa SendAsync(HttpRequestMessage) för att skicka begäranden till SUT. Överbelastningar för SendAsync accepterar HTML-formuläret (IHtmlFormElement) och följande:
    • Knappen Skicka i formuläret (IHtmlElement)
    • Samling med formulärvärden (IEnumerable<KeyValuePair<string, string>>)
    • Knappen Skicka (IHtmlElement) och formulärvärden (IEnumerable<KeyValuePair<string, string>>)

AngleSharp är ett tredjepartsbibliotek som används för demonstrationsändamål i den här artikeln och exempelappen. AngleSharp stöds inte eller krävs inte för integreringstestning av ASP.NET Core-appar. Andra parsers kan användas, till exempel HTML Agility Pack (HAP). En annan metod är att skriva kod för att hantera antiforgery-systemets verifieringstoken för begäran och cookie direkt. Mer information finns i AngleSharp vs Application Parts för förfalskningskontroller i den här artikeln.

Den EF-Core minnesinterna databasprovidern kan användas för begränsad och grundläggande testning, men SQLite-providern är det rekommenderade valet för minnesintern testning.

Se Utöka start med startfilter som visar hur du konfigurerar mellanprogram med IStartupFilter, vilket är användbart när ett test kräver en anpassad tjänst eller mellanprogram.

Anpassa klienten med WithWebHostBuilder

När ytterligare konfiguration krävs inom en testmetod skapar WithWebHostBuilder en ny WebApplicationFactory med en IWebHostBuilder som anpassas ytterligare efter konfiguration.

-exempelkoden anropar WithWebHostBuilder för att ersätta konfigurerade tjänster med teststubbar. För mer information och exempel på användning, se Injicera simulerade tjänster i den här artikeln.

Post_DeleteMessageHandler_ReturnsRedirectToRoot testmetoden för exempelappen visar användningen av WithWebHostBuilder. Det här testet utför en postborttagning i databasen genom att utlösa en formulärinlämning i SUT.

Eftersom ett annat test i klassen IndexPageTests utför en åtgärd som tar bort alla poster i databasen och kan köras före metoden Post_DeleteMessageHandler_ReturnsRedirectToRoot, återställs databasen i den här testmetoden för att säkerställa att det finns en post för SUT att ta bort. Att välja den första borttagningsknappen i formuläret messages i SUT simuleras i en begäran till 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);
}

Klientalternativ

Se sidan WebApplicationFactoryClientOptions för standardvärden och tillgängliga alternativ när du skapar HttpClient instanser.

Skapa klassen WebApplicationFactoryClientOptions och skicka den till metoden CreateClient():

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

OBS! Om du vill undvika HTTPS-omdirigeringsvarningar i loggar när du använder HTTPS Redirection Middleware anger du BaseAddress = new Uri("https://localhost")

Mata in falska tjänster

Tjänster kan åsidosättas i ett test med ett anrop till ConfigureTestServices på värdverktyget. Om du vill begränsa de åsidosatta tjänsterna till själva testet används WithWebHostBuilder-metoden för att hämta en värdbyggare. Detta kan visas i följande tester:

Exempel-SUT innehåller en avgränsad tjänst som returnerar en offert. Citatet bäddas in i ett dolt fält på Indexsidan när Indexsidan begärs.

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

Följande markering genereras när SUT-appen körs:

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

För att testa tjänsten och offertinmatningen i ett integreringstest matas en modelltjänst in i SUT:en av testet. Mock-tjänsten ersätter appens QuoteService med en tjänst som tillhandahålls av testappen med namnet 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 anropas, och den avgränsade tjänsten registreras.

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

Markeringen som producerades under testets körning återspeglar citattexten som given av TestQuoteService, vilket innebär att påståendet klarar:

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

Simulera autentisering

Tester i klassen AuthTests kontrollerar att en säker slutpunkt finns:

  • Omdirigerar en oautentiserad användare till appens inloggningssida.
  • Returnerar innehåll för en autentiserad användare.

På SUT använder sidan /SecurePage en konvention med AuthorizePage för att tillämpa en AuthorizeFilter. Mer information finns i Razor Pages-auktoriseringskonventioner.

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

I det Get_SecurePageRedirectsAnUnauthenticatedUser testet är en WebApplicationFactoryClientOptions inställd på att inte tillåta omdirigeringar genom att ange AllowAutoRedirect till false:

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

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

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

Genom att inte tillåta att klienten följer omdirigeringen kan följande kontroller göras:

  • Statuskoden som returneras av SUT kan kontrolleras mot det förväntade HttpStatusCode.Redirect resultatet, inte den slutliga statuskoden efter omdirigeringen till inloggningssidan, vilket skulle vara HttpStatusCode.OK.
  • Det Location-sidhuvudvärdet i svarshuvudena kontrolleras för att bekräfta att det börjar med http://localhost/Identity/Account/Login, inte svaret på den sista inloggningssidan, där Location-huvudet inte skulle finnas.

Testappen kan simulera en AuthenticationHandler<TOptions> i ConfigureTestServices för att testa autentiserings- och auktoriseringsaspekter. Ett minimalt scenario returnerar en 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);
    }
}

TestAuthHandler anropas för att autentisera en användare när autentiseringsschemat är inställt på TestScheme där AddAuthentication har registrerats för ConfigureTestServices. Det är viktigt att TestScheme-schemat matchar det schema som din app förväntar sig. Annars fungerar inte autentiseringen.

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

Mer information om WebApplicationFactoryClientOptionsfinns i avsnittet Klientalternativ.

Grundläggande tester för mellanprogram för autentisering

Se den här GitHub-lagringsplatsen för grundläggande tester av mellanprogram för autentisering. Den innehåller en testserver som är specifik för testscenariot.

Ange miljön

Ange miljö i den anpassade programfabriken:

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

Hur testinfrastrukturen bestämmer appens rotväg för innehåll

WebApplicationFactory konstruktorn härleder appen innehållsrotens sökväg genom att söka efter en WebApplicationFactoryContentRootAttribute på sammansättningen som innehåller integreringstesterna med en nyckel som är lika med TEntryPoint sammansättning System.Reflection.Assembly.FullName. Om ett attribut med rätt nyckel inte hittas återgår WebApplicationFactory till att söka efter en lösningsfil (.sln) och lägger till TEntryPoint sammansättningsnamn i lösningskatalogen. Appens rotkatalog (innehållsrotsökvägen) används för att identifiera vyer och innehållsfiler.

Inaktivera skuggkopiering

Skuggkopiering gör att testerna körs i en annan katalog än utdatakatalogen. Om dina tester förlitar sig på att läsa in filer i förhållande till Assembly.Location och du stöter på problem kan du behöva inaktivera skuggkopiering.

Om du vill inaktivera skuggkopiering när du använder xUnit skapar du en xunit.runner.json fil i testprojektkatalogen med rätt konfigurationsinställning:

{
  "shadowCopy": false
}

Bortskaffande av objekt

När testerna av IClassFixture-implementeringen har körts tas TestServer och HttpClient bort när xUnit tar bort WebApplicationFactory. Om objekt som instansieras av utvecklaren kräver bortskaffande ska du ta bort dem i IClassFixture implementeringen. Mer information finns i Implementera en dispose-metod.

Exempel på integreringstester

Den exempelappen består av två appar:

Applikation Projektkatalog Beskrivning
Meddelandeapp (SUT) src/RazorPagesProject Tillåter att en användare lägger till, tar bort en, tar bort alla och analyserar meddelanden.
Testapp tests/RazorPagesProject.Tests Används för att utföra integrationstest av SUT.

Testerna kan köras med hjälp av de inbyggda testfunktionerna i en IDE, till exempel Visual Studio. Om du använder Visual Studio Code eller kommandoraden, kör följande kommando i en kommandotolk i katalogen tests/RazorPagesProject.Tests:

dotnet test

Meddelandeappsorganisation (SUT)

SUT är ett meddelandesystem för Razor Pages med följande egenskaper:

  • Sidan Index i appen (Pages/Index.cshtml och Pages/Index.cshtml.cs) innehåller ett användargränssnitt och sidmodellmetoder för att styra tillägg, borttagning och analys av meddelanden (genomsnittliga ord per meddelande).
  • Ett meddelande beskrivs av klassen Message (Data/Message.cs) med två egenskaper: Id (nyckel) och Text (meddelande). Egenskapen Text krävs och är begränsad till 200 tecken.
  • Meddelanden lagras med hjälp av Entity Frameworks minnesinterna databas†.
  • Appen innehåller ett dataåtkomstlager (DAL) i sin databaskontextklass AppDbContext (Data/AppDbContext.cs).
  • Om databasen är tom vid appstart initieras meddelandearkivet med tre meddelanden.
  • Appen innehåller en /SecurePage som bara kan nås av en autentiserad användare.

† EF-artikeln, Test with InMemory, förklarar hur du använder en minnesintern databas för tester med MSTest. Det här avsnittet använder testramverket xUnit. Testbegrepp och testimplementeringar i olika testramverk är liknande men inte identiska.

Även om appen inte använder lagringsplatsens mönster och inte är ett effektivt exempel på UoW-mönstret (Unit of Work), stöder Razor Pages dessa utvecklingsmönster. Mer information finns i Utformning av infrastrukturens persistenslager och testa styrenhetslogik (exemplet implementerar lagerlagringsmönstret).

Testapporganisation

Testappen är en konsolapp i katalogen tests/RazorPagesProject.Tests.

Testappkatalog Beskrivning
AuthTests Innehåller testmetoder för:
  • Åtkomst till en säker sida av en oautentiserad användare.
  • En autentiserad användare med en falsk AuthenticationHandler<TOptions>går in på en säker sida.
  • Hämta en GitHub-användarprofil och kontrollera profilens användarinloggning.
BasicTests Innehåller en testmetod för routning och innehållstyp.
IntegrationTests Innehåller integreringstesterna för sidan Index med anpassad WebApplicationFactory-klass.
Helpers/Utilities
  • Utilities.cs innehåller den InitializeDbForTests metod som används för att seeda databasen med testdata.
  • HtmlHelpers.cs tillhandahåller en metod för att returnera en AngleSharp-IHtmlDocument för användning i testmetoder.
  • HttpClientExtensions.cs tillhandahålla överlagringar för SendAsync att skicka begäranden till SUT.

Testramverket är xUnit. Integreringstester utförs med hjälp av Microsoft.AspNetCore.TestHost, som innehåller TestServer. Eftersom Microsoft.AspNetCore.Mvc.Testing-paketet används för att konfigurera testvärd och testservern, behöver TestHost- och TestServer-paketen inte direkta paketreferenser i testappens projektfil eller utvecklarkonfiguration i testappen.

Integreringstester kräver vanligtvis en liten datauppsättning i databasen före testkörningen. Ett borttagningstest anropar till exempel en borttagning av en databaspost, så databasen måste ha minst en post för att borttagningsbegäran ska lyckas.

Exempelappen initierar databasen med tre meddelanden i Utilities.cs som testerna kan använda när de körs.

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

SUT:s databaskontext registreras i Program.cs. Testappens builder.ConfigureServices återanrop körs efter att appens Program.cs kod har körts. Om du vill använda en annan databas för testerna måste appens databaskontext ersättas i builder.ConfigureServices. Mer information finns i avsnittet Anpassa WebApplicationFactory.

Ytterligare resurser

Det här avsnittet förutsätter en grundläggande förståelse av enhetstester. Om du inte känner till testbegrepp kan du läsa avsnittet Enhetstestning i .NET Core och .NET Standard och dess länkade innehåll.

Visa eller ladda ned exempelkod (hur du laddar ned)

Exempelappen är en Razor Pages-app och förutsätter en grundläggande förståelse för Razor Pages. Om du inte känner till Razor sidor kan du läsa följande avsnitt:

Anmärkning

För att testa SPA:er rekommenderar vi ett verktyg som Playwright för .NET, som kan automatisera en webbläsare.

Introduktion till integreringstester

Integreringstester utvärderar en apps komponenter på en bredare nivå än enhetstester. Enhetstester används för att testa isolerade programvarukomponenter, till exempel enskilda klassmetoder. Integreringstester bekräftar att två eller flera appkomponenter fungerar tillsammans för att skapa ett förväntat resultat, eventuellt inklusive varje komponent som krävs för att bearbeta en begäran fullt ut.

Dessa bredare tester används för att testa appens infrastruktur och hela ramverket, ofta med följande komponenter:

  • Databas
  • Filsystem
  • Nätverksinstallationer
  • Pipeline för begärandesvar

Enhetstester använder fabricerade komponenter, så kallade falska eller falska objekt, i stället för infrastrukturkomponenter.

Till skillnad från enhetstester, integreringstester:

  • Använd de faktiska komponenter som appen använder i produktion.
  • Kräv mer kod och databearbetning.
  • Det tar längre tid att köra.

Begränsa därför användningen av integreringstester till de viktigaste infrastrukturscenarierna. Om ett beteende kan testas med antingen ett enhetstest eller ett integreringstest väljer du enhetstestet.

I diskussioner om integreringstester kallas det testade projektet ofta System Under Test, eller "SUT" för kort. "SUT" används i hela den här artikeln för att referera till ASP.NET Core-appen som testas.

Skriv inte integreringstester för varje permutation av data och filåtkomst med databaser och filsystem. Oavsett hur många platser i en app som interagerar med databaser och filsystem kan en prioriterad uppsättning av integreringstester för läsning, skrivning, uppdatering och borttagning vanligtvis testa databas- och filsystemkomponenter på ett tillfredsställande sätt. Använd enhetstester för rutinmässiga tester av metodlogik som interagerar med dessa komponenter. I enhetstester resulterar användningen av fake- eller mock-infrastruktur i snabbare testkörning.

ASP.NET Core-integreringstester

Integreringstester i ASP.NET Core kräver följande:

  • Ett testprojekt används för att innehålla och köra testerna. Testprojektet har en referens till SUT.
  • Testprojektet skapar en testwebbvärd för SUT och använder en testserverklient för att hantera begäranden och svar med SUT.
  • En testkörare används för att köra testerna och rapportera testresultaten.

Integreringstester följer en sekvens av händelser som innefattar de vanliga teststegen Ordna, Ageraoch Bekräfta.

  1. SUT:s webbhotell är konfigurerat.
  2. En testserverklient skapas för att skicka begäranden till appen.
  3. Teststeget Ordna körs: Testappen förbereder en begäran.
  4. Teststeget Act körs: Klienten skickar begäran och tar emot svaret.
  5. Teststeget Assert körs: Det faktiska svaret verifieras som ett godkänt eller underkänd baserat på ett förväntat svar.
  6. Processen fortsätter tills alla tester körs.
  7. Testresultaten rapporteras.

Vanligtvis är testwebbvärden konfigurerad på ett annat sätt än appens normala webbvärd för testkörningarna. Till exempel kan en annan databas eller olika appinställningar användas för testerna.

Infrastrukturkomponenter, till exempel testwebbvärden och minnesintern testserver (TestServer), tillhandahålls eller hanteras av Microsoft.AspNetCore.Mvc.Testing-paketet. Användning av det här paketet effektiviserar skapande och körning av test.

Microsoft.AspNetCore.Mvc.Testing-paketet hanterar följande uppgifter:

  • Kopierar beroendefilen (.deps) från SUT till testprojektets bin katalog.
  • Anger innehållsroten till SUT:s projektrot så att statiska filer och sidor/vyer hittas när testerna körs.
  • Tillhandahåller klassen WebApplicationFactory för att strömlinjeforma uppstarten av SUT med TestServer.

I enhetstester dokumentationen beskrivs hur du konfigurerar ett testprojekt och en testlöpare, tillsammans med detaljerade instruktioner om hur du kör tester och rekommendationer för hur du namnger tester och testklasser.

Separera enhetstester från integreringstester i olika projekt. Att separera testerna:

  • Hjälper till att säkerställa att komponenter för infrastrukturtestning inte oavsiktligt ingår i enhetstesterna.
  • Tillåter kontroll över vilken uppsättning tester som körs.

Det finns praktiskt taget ingen skillnad mellan konfigurationen för tester av Razor Pages-appar och MVC-appar. Den enda skillnaden är hur testerna namnges. I en Razor Pages-app namnges vanligtvis tester av sidslutpunkter efter sidmodellklassen (till exempel IndexPageTests för att testa komponentintegrering för indexsidan). I en MVC-app ordnas tester vanligtvis efter kontrollantklasser och namnges efter de kontrollanter som de testar (till exempel HomeControllerTests för att testa komponentintegrering för Home kontrollanten).

Krav för testapp

Testprojektet måste:

Dessa förutsättningar kan ses i exempelappen. Granska filen tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj. Exempelappen använder testramverket xUnit och AngleSharp parser-biblioteket, så exempelappen refererar också till:

I appar som använder xunit.runner.visualstudio version 2.4.2 eller senare måste testprojektet referera till Microsoft.NET.Test.Sdk-paketet.

Entity Framework Core används också i testerna. Appen refererar till:

SUT-miljö

Om SUT:s miljö inte har angetts, är standardmiljön Utveckling.

Grundläggande tester med standard-WebApplicationFactory

WebApplicationFactory<TEntryPoint> används för att skapa en TestServer för integreringstesterna. TEntryPoint är startpunktsklassen för SUT, vanligtvis klassen Startup.

Testklasser implementerar ett klassfixtur gränssnitt (IClassFixture) för att indikera att klassen innehåller tester och tillhandahåller delade objektinstanser i testerna i klassen.

Följande testklass, BasicTests, använder WebApplicationFactory för att starta SUT och tillhandahålla en HttpClient till en testmetod, Get_EndpointsReturnSuccessAndCorrectContentType. Metoden kontrollerar om svarsstatuskoden är framgångsrik (statuskoder i intervallet 200–299) och Content-Type-huvudet är text/html; charset=utf-8 för flera av appens sidor.

CreateClient() skapar en instans av HttpClient som automatiskt följer omdirigeringar och hanterar cookies.

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

Som standard bevaras inte icke-nödvändiga cookies mellan begäranden när GDPR-samtyckespolicy är aktiverad. Om du vill bevara icke-nödvändiga cookies, till exempel de som används av TempData-providern, markerar du dem som viktiga i dina tester. Anvisningar om hur du markerar ett cookie som viktigt finns i Viktiga cookies.

Anpassa WebApplicationFactory

Webbhotellkonfiguration kan skapas oberoende av testklasserna genom att ärva från WebApplicationFactory för att skapa en eller flera anpassade fabrikatorer.

  1. Ärv från WebApplicationFactory och åsidosätt ConfigureWebHost. Med IWebHostBuilder kan du konfigurera tjänstsamlingen med 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);
                    }
                }
            });
        }
    }
    

    Databasutsöndring i exempelappen utförs av metoden InitializeDbForTests. Metoden beskrivs i exemplet med integrationstester: avsnittet Testa apporganisationen.

    SUT:s databaskontext är registrerad i dess Startup.ConfigureServices-metod. Testappens builder.ConfigureServices återanrop körs efter att appens Startup.ConfigureServices kod har körts. Körningsordningen är en viktig ändring för Generic Host i utgåvan av ASP.NET Core 3.0. Om du vill använda en annan databas för testerna än appens databas måste appens databaskontext ersättas i builder.ConfigureServices.

    För SUT:er som fortfarande använder Web Hostkörs testappens builder.ConfigureServices återanrop innan SUT:s Startup.ConfigureServices kod. Testappens builder.ConfigureTestServices återanrop körs efter.

    Exempelappen hittar tjänstbeskrivningen för databaskontexten och använder beskrivningen för att ta bort tjänstregistreringen. Därefter lägger fabriken till en ny ApplicationDbContext som använder en minnesintern databas för testerna.

    Om du vill ansluta till en annan databas än den minnesinterna databasen ändrar du UseInMemoryDatabase-anropet för att ansluta kontexten till en annan databas. Så här använder du en SQL Server-testdatabas:

    services.AddDbContext<ApplicationDbContext>((options, context) => 
    {
        context.UseSqlServer(
            Configuration.GetConnectionString("TestingDbConnectionString"));
    });
    
  2. Använd anpassade CustomWebApplicationFactory i testklasser. I följande exempel används fabriken i klassen IndexPageTests:

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

    Exempelappens klient är konfigurerad för att förhindra att HttpClient följer omdirigeringar. Som beskrivs senare i avsnittet Mock authentication tillåter detta tester att kontrollera resultatet av appens första svar. Det första svaret är en omdirigering i många av dessa tester med ett Location-huvud.

  3. Ett typiskt test använder HttpClient- och hjälpmetoderna för att bearbeta begäran och svaret:

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

Alla POST-begäranden till SUT måste uppfylla den antiförfalskningskontroll som automatiskt utförs av appens dataskyddssystem. För att kunna ställa in ett testets POST-begäran måste testappen:

  1. Gör en begäran för sidan.
  2. Tolka antiforgery-cookie och hämta valideringstoken från svaret.
  3. Gör POST-begäran med antiforgery-cookie och begär valideringstoken på plats.

SendAsync-tilläggsmetoderna (Helpers/HttpClientExtensions.cs) och GetDocumentAsync-hjälpmetoden (Helpers/HtmlHelpers.cs) i exempelappen använder AngleSharp -parsern för att hantera förfalskningskontrollen med följande metoder:

  • GetDocumentAsync: Tar emot HttpResponseMessage och returnerar en IHtmlDocument. GetDocumentAsync använder en fabrik som förbereder ett virtuellt svar baserat på den ursprungliga HttpResponseMessage. Mer information finns i AngleSharp-dokumentationen.
  • SendAsync tilläggsmetoder för HttpClient skapa en HttpRequestMessage och anropa SendAsync(HttpRequestMessage) för att skicka begäranden till SUT. Överbelastningar för SendAsync accepterar HTML-formuläret (IHtmlFormElement) och följande:
    • Knappen Skicka i formuläret (IHtmlElement)
    • Samling med formulärvärden (IEnumerable<KeyValuePair<string, string>>)
    • Knappen Skicka (IHtmlElement) och formulärvärden (IEnumerable<KeyValuePair<string, string>>)

Anmärkning

AngleSharp är ett bibliotek från tredje part som används för demonstration i det här avsnittet och exempelappen. AngleSharp stöds inte eller krävs inte för integreringstestning av ASP.NET Core-appar. Andra parsers kan användas, till exempel HTML Agility Pack (HAP). En annan metod är att skriva kod för att hantera antiforgery-systemets verifieringstoken för begäran och cookie direkt.

Anmärkning

Den EF-Core minnesinterna databasprovidern kan användas för begränsad och grundläggande testning, men SQLite-providern är det rekommenderade valet för minnesintern testning.

Anpassa klienten med WithWebHostBuilder

När ytterligare konfiguration krävs inom en testmetod skapar WithWebHostBuilder en ny WebApplicationFactory med en IWebHostBuilder som anpassas ytterligare efter konfiguration.

Testmetoden Post_DeleteMessageHandler_ReturnsRedirectToRoot för exempelappen demonstrerar användningen av WithWebHostBuilder. Det här testet utför en postborttagning i databasen genom att utlösa en formulärinlämning i SUT.

Eftersom ett annat test i klassen IndexPageTests utför en åtgärd som tar bort alla poster i databasen och kan köras före metoden Post_DeleteMessageHandler_ReturnsRedirectToRoot, återställs databasen i den här testmetoden för att säkerställa att det finns en post för SUT att ta bort. Att välja den första raderingsknappen i formulär messages i SUT simuleras i begäran till 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);
}

Klientalternativ

I följande tabell visas den tillgängliga standarden för WebApplicationFactoryClientOptions när du skapar instanser av HttpClient.

Alternativ Beskrivning Standardinställning
AllowAutoRedirect Hämtar eller anger om HttpClient instanser ska följa omdirigeringssvaren automatiskt. true
BaseAddress Hämtar eller anger basadressen för HttpClient instanser. http://localhost
HandleCookies Hämtar eller anger om HttpClient instanser ska hantera cookies. true
MaxAutomaticRedirections Hämtar eller anger det maximala antalet omdirigeringssvar som HttpClient instanser ska följa. 7

Skapa klassen WebApplicationFactoryClientOptions och skicka den till metoden CreateClient() (standardvärden visas i kodexemplet):

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

Infoga mocktjänster

Tjänster kan åsidosättas med ett anrop till ConfigureTestServices på värdbyggaren i ett test. Om du vill mata in falska tjänster måste SUT ha en Startup-klass med en Startup.ConfigureServices-metod.

Exempel-SUT innehåller en tjänst med begränsat omfång som returnerar en offert. Citatet bäddas in i ett dolt fält på indexsidan när sidan begärs.

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

Följande markering genereras när SUT-appen körs:

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

För att testa tjänsten och offertinmatningen i ett integreringstest matas en modelltjänst in i SUT:en av testet. Mock-tjänsten ersätter appens QuoteService med en tjänst som tillhandahålls av testappen med namnet 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 anropas och den begränsade tjänsten registreras:

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

Markeringen som producerades under testets körning återspeglar citattexten som tillhandahålls av TestQuoteService, vilket innebär att kontrollen godkänns.

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

Simulera autentisering

Tester i klassen AuthTests kontrollerar att en säker slutpunkt:

  • Omdirigerar en oautentiserad användare till appens inloggningssida.
  • Returnerar innehåll för en autentiserad användare.

På sidan SUT använder /SecurePage en AuthorizePage konvention för att tillämpa en AuthorizeFilter på sidan. Mer information finns i Razor Pages-auktoriseringskonventioner.

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

I det Get_SecurePageRedirectsAnUnauthenticatedUser-testet är en WebApplicationFactoryClientOptions inställd på att inte tillåta omdirigeringar genom att ange AllowAutoRedirect till false.

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

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

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login", 
        response.Headers.Location.OriginalString);
}

Genom att inte tillåta att klienten följer omdirigeringen kan följande kontroller göras:

  • Statuskoden som returneras av SUT kan kontrolleras mot det förväntade HttpStatusCode.Redirect resultat, inte den slutliga statuskoden efter omdirigeringen till inloggningssidan, vilket skulle vara HttpStatusCode.OK.
  • Värdet för Location-huvudet i svarshuvudena kontrolleras för att bekräfta att det börjar med http://localhost/Identity/Account/Login, inte det slutliga svaret på inloggningssidan, där Location-huvudet inte skulle finnas.

Testappen kan simulera en AuthenticationHandler<TOptions> i ConfigureTestServices för att testa funktioner av autentisering och behörighetskontroll. Ett minimalt scenario returnerar en 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);
    }
}

TestAuthHandler anropas för att autentisera en användare när autentiseringsschemat är inställt på Test där AddAuthentication har registrerats för ConfigureTestServices. Det är viktigt att Test-schemat matchar det schema som din app förväntar sig. Annars fungerar inte autentiseringen.

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

Mer information om WebApplicationFactoryClientOptionsfinns i avsnittet Klientalternativ.

Ange miljön

Som standard är SUT:s värd- och appmiljö konfigurerad att använda utvecklingsmiljön. Så här åsidosätter du SUT-miljön när du använder IHostBuilder:

  • Ange miljövariabeln ASPNETCORE_ENVIRONMENT (till exempel Staging, Productioneller annat anpassat värde, till exempel Testing).
  • Överskriv CreateHostBuilder i testappen för att läsa miljövariabler med prefixet ASPNETCORE.
protected override IHostBuilder CreateHostBuilder() =>
    base.CreateHostBuilder()
        .ConfigureHostConfiguration(
            config => config.AddEnvironmentVariables("ASPNETCORE"));

Om SUT använder webbservern (IWebHostBuilder), åsidosätt CreateWebHostBuilder:

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

Hur testinfrastrukturen fastställer appens innehållsrotväg

WebApplicationFactory konstruktorn härleder appen innehållsrotens sökväg genom att söka efter en WebApplicationFactoryContentRootAttribute på sammansättningen som innehåller integreringstesterna med en nyckel som är lika med TEntryPoint sammansättning System.Reflection.Assembly.FullName. Om ett attribut med rätt nyckel inte hittas återgår WebApplicationFactory till att söka efter en lösningsfil (.sln) och lägger till TEntryPoint sammansättningsnamn i lösningskatalogen. Appens rotkatalog (innehållsrotsökvägen) används för att identifiera vyer och innehållsfiler.

Inaktivera skuggkopiering

Skuggkopiering gör att testerna körs i en annan katalog än utdatakatalogen. Om dina tester förlitar sig på att läsa in filer i förhållande till Assembly.Location och du stöter på problem kan du behöva inaktivera skuggkopiering.

Om du vill inaktivera skuggkopiering när du använder xUnit skapar du en xunit.runner.json fil i testprojektkatalogen med rätt konfigurationsinställning:

{
  "shadowCopy": false
}

Bortskaffande av objekt

När testerna av IClassFixture-implementeringen har körts tas TestServer och HttpClient bort när xUnit tar bort WebApplicationFactory. Om objekt som instansieras av utvecklaren kräver bortskaffande ska du ta bort dem i IClassFixture implementeringen. Mer information finns i Implementera en dispose-metod.

Exempel på integreringstester

Den exempelappen består av två appar:

Applikation Projektkatalog Beskrivning
Meddelandeapp (SUT) src/RazorPagesProject Tillåter att en användare lägger till, tar bort en, tar bort alla och analyserar meddelanden.
Testapp tests/RazorPagesProject.Tests Används för att integrera test av SUT.

Testerna kan köras med hjälp av de inbyggda testfunktionerna i en IDE, till exempel Visual Studio. Om du använder Visual Studio Code- eller kommandoraden kör du följande kommando i en kommandotolk i katalogen tests/RazorPagesProject.Tests:

dotnet test

Meddelandeappsorganisation (SUT)

SUT är ett meddelandesystem för Razor Pages med följande egenskaper:

  • Sidan Index i appen (Pages/Index.cshtml och Pages/Index.cshtml.cs) innehåller ett användargränssnitt och sidmodellmetoder för att styra tillägg, borttagning och analys av meddelanden (genomsnittliga ord per meddelande).
  • Ett meddelande beskrivs av klassen Message (Data/Message.cs) med två egenskaper: Id (nyckel) och Text (meddelande). Egenskapen Text krävs och är begränsad till 200 tecken.
  • Meddelanden lagras med hjälp av Entity Frameworks minnesinterna databas†.
  • Appen innehåller ett dataåtkomstlager (DAL) i sin databaskontextklass AppDbContext (Data/AppDbContext.cs).
  • Om databasen är tom vid appstart initieras meddelandearkivet med tre meddelanden.
  • Appen innehåller en /SecurePage som bara kan nås av en autentiserad användare.

† EF-ämnet, Test with InMemory, förklarar hur du använder en minnesintern databas för tester med MSTest. Det här avsnittet använder testramverket xUnit. Testbegrepp och testimplementeringar i olika testramverk är liknande men inte identiska.

Även om appen inte använder lagringsplatsens mönster och inte är ett effektivt exempel på UoW-mönstret (Unit of Work), stöder Razor Pages dessa utvecklingsmönster. Mer information finns i Utforma lagringslagret för infrastruktur och testkontrollerns logik (exemplet implementerar repositoriemönstret).

Testapplikationens organisation

Testappen är en konsolapp i katalogen tests/RazorPagesProject.Tests.

Appkatalog för test Beskrivning
AuthTests Innehåller testmetoder för:
  • Åtkomst till en säker sida av en oautentiserad användare.
  • Åtkomst till en säker sida av en autentiserad användare med en falsk AuthenticationHandler<TOptions>.
  • Hämta en GitHub-användarprofil och kontrollera profilens användarinloggning.
BasicTests Innehåller en testmetod för routning och innehållstyp.
IntegrationTests Innehåller integreringstesterna för sidan Index med anpassad WebApplicationFactory-klass.
Helpers/Utilities
  • Utilities.cs innehåller den InitializeDbForTests metod som används för att seeda databasen med testdata.
  • HtmlHelpers.cs tillhandahåller en metod för att returnera en AngleSharp-IHtmlDocument för användning av testmetoderna.
  • HttpClientExtensions.cs tillhandahålla överbelastningar så att SendAsync kan skicka begäranden till SUT.

Testramverket är xUnit. Integreringstester utförs med hjälp av Microsoft.AspNetCore.TestHost, som innehåller TestServer. Eftersom Microsoft.AspNetCore.Mvc.Testing-paketet används för att konfigurera testvärden och testservern behöver TestHost- och TestServer-paketen inte ha direkta paketreferenser i testappens projektfil eller utvecklarkonfiguration i testappen.

Integreringstester kräver vanligtvis en liten datauppsättning i databasen före testkörningen. Ett borttagningstest anropar till exempel en borttagning av en databaspost, så databasen måste ha minst en post för att borttagningsbegäran ska lyckas.

Exempelappen seedar databasen med tre meddelanden i Utilities.cs som testerna kan använda när de exekveras.

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

SUT:s databaskontext är registrerad i dess Startup.ConfigureServices-metod. Testappens callback-funktion builder.ConfigureServices körs efter att appens Startup.ConfigureServices kod har körts. Om du vill använda en annan databas för testerna måste appens databaskontext ersättas i builder.ConfigureServices. Mer information finns i avsnittet Anpassa WebApplicationFactory.

För SUT:er som fortfarande använder Web Hostkörs testappens builder.ConfigureServices återanrop innan SUT:s Startup.ConfigureServices kod. Testappens builder.ConfigureTestServices återanrop körs efter.

Ytterligare resurser

Den här artikeln förutsätter en grundläggande förståelse av enhetstester. Om du inte är bekant med testbegrepp kan du läsa artikeln Enhetstestning i .NET Core och .NET Standard och dess länkade innehåll.

Visa eller ladda ned exempelkod (hur du laddar ned)

Exempelappen är en Razor Pages-app och förutsätter en grundläggande förståelse för Razor Pages. Om du inte är bekant med Razor Pages kan du läsa följande artiklar:

För att testa SPA:errekommenderar vi ett verktyg som Playwright för .NET, som kan automatisera en webbläsare.

Introduktion till integreringstester

Integreringstester utvärderar en apps komponenter på en bredare nivå än enhetstester. Enhetstester används för att testa isolerade programvarukomponenter, till exempel enskilda klassmetoder. Integreringstester bekräftar att två eller flera appkomponenter fungerar tillsammans för att skapa ett förväntat resultat, eventuellt inklusive varje komponent som krävs för att bearbeta en begäran fullt ut.

Dessa bredare tester används för att testa appens infrastruktur och hela ramverket, ofta med följande komponenter:

  • Databas
  • Filsystem
  • Nätverksinstallationer
  • Pipeline för begärandesvar

Enhetstester använder fabricerade komponenter, så kallade falska eller mockobjekt, i stället för infrastrukturkomponenter.

Till skillnad från enhetstester, integreringstester:

  • Använd de faktiska komponenter som appen använder i produktion.
  • Kräv mer kod och databearbetning.
  • Ta längre tid att köra.

Begränsa därför användningen av integreringstester till de viktigaste infrastrukturscenarierna. Om ett beteende kan testas med antingen ett enhetstest eller ett integreringstest väljer du enhetstestet.

I diskussioner om integreringstester kallas det testade projektet ofta System Under Test, eller "SUT" för kort. "SUT" används i hela den här artikeln för att referera till ASP.NET Core-appen som testas.

Skriv inte integreringstester för varje permutation av data och filåtkomst med databaser och filsystem. Oavsett hur många platser i en app som interagerar med databaser och filsystem kan en prioriterad uppsättning av integreringstester för läsning, skrivning, uppdatering och borttagning vanligtvis testa databas- och filsystemkomponenter på ett tillfredsställande sätt. Använd enhetstester för rutinmässiga tester av metodlogik som interagerar med dessa komponenter. I enhetstester resulterar användningen av infrastrukturfalskningar eller mockobjekt i snabbare testkörning.

ASP.NET Core-integreringstester

Integreringstester i ASP.NET Core kräver följande:

  • Ett testprojekt används för att innehålla och köra testerna. Testprojektet har en referens till SUT.
  • Testprojektet skapar en testwebbvärd för SUT och använder en testserverklient för att hantera begäranden och svar med SUT.
  • En testkörare används för att köra testerna och rapportera testresultaten.

Integreringstester följer en sekvens med händelser som innehåller de vanliga teststegen Ordna, Ageraoch Bekräfta.

  1. SUT:s webbhotell är konfigurerat.
  2. En testserverklient skapas för att skicka begäranden till appen.
  3. Teststeget Ordna körs: Testappen förbereder en begäran.
  4. Teststeget Act körs: Klienten skickar begäran och tar emot svaret.
  5. Teststeget Assert körs: Det faktiska svaret verifieras som ett godkänd eller underkänd baserat på det förväntade svaret.
  6. Processen fortsätter tills alla tester körs.
  7. Testresultaten rapporteras.

Vanligtvis är testservern konfigurerad på ett annat sätt än applikationens vanliga server för testerna. Till exempel kan en annan databas eller olika appinställningar användas för testerna.

Infrastrukturkomponenter, till exempel testwebbvärden och minnesintern testserver (TestServer), tillhandahålls eller hanteras av Microsoft.AspNetCore.Mvc.Testing-paketet. Användning av det här paketet effektiviserar skapande och körning av test.

Microsoft.AspNetCore.Mvc.Testing-paketet hanterar följande uppgifter:

  • Kopierar beroendefilen (.deps) från SUT till testprojektets bin katalog.
  • Anger innehållsroten till projektroten för SUT så att statiska filer och sidor/vyer hittas när testerna körs.
  • Tillhandahåller klassen WebApplicationFactory för att effektivisera uppstarten av SUT med TestServer.

I enhetstester dokumentationen beskrivs hur du konfigurerar ett testprojekt och en testlöpare, tillsammans med detaljerade instruktioner om hur du kör tester och rekommendationer för hur du namnger tester och testklasser.

Separera enhetstester från integreringstester i olika projekt. Att separera testerna:

  • Hjälper till att säkerställa att komponenter för infrastrukturtestning inte oavsiktligt ingår i enhetstesterna.
  • Tillåter kontroll över vilken uppsättning tester som körs.

Det finns praktiskt taget ingen skillnad mellan konfigurationen för tester av Razor Pages-appar och MVC-appar. Den enda skillnaden är hur testerna namnges. I en Razor Pages-app namnges vanligtvis tester av sidslutpunkter efter sidmodellklassen (till exempel IndexPageTests för att testa komponentintegrering för indexsidan). I en MVC-app ordnas tester vanligtvis efter kontrollantklasser och namnges efter de kontrollanter som de testar (till exempel HomeControllerTests för att testa komponentintegrering för Home kontrollanten).

Krav för testapp

Testprojektet måste:

Dessa förutsättningar kan ses i exempelappen. Inspektera tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj-filen. Exempelappen använder testramverket xUnit och AngleSharp parser-biblioteket, så exempelappen refererar också till:

I appar som använder xunit.runner.visualstudio version 2.4.2 eller senare måste testprojektet referera till Microsoft.NET.Test.Sdk-paketet.

Entity Framework Core används också i testerna. Se -projektfilen i GitHub.

SUT-miljö

Om SUT:s miljö inte är angiven, blir standardmiljön Utveckling.

Grundläggande tester med standard-WebApplicationFactory

Exponera den implicit definierade Program-klassen för testprojektet genom att göra något av följande:

  • Gör interna typer från webbappen tillgängliga för testprojektet. Detta kan göras i SUT-projektets fil (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Gör Program-klassen offentlig med hjälp av en partiell klass-deklaration:

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

    Den exempelappen använder Program partiell klassmetod.

WebApplicationFactory<TEntryPoint> används för att skapa en TestServer för integreringstesterna. TEntryPoint är startpunktsklassen för SUT, vanligtvis Program.cs.

Testklasser implementerar ett klassfixtur gränssnitt (IClassFixture) för att visa att klassen innehåller tester och möjliggör delade objektinstanser mellan testerna i klassen.

Följande testklass, BasicTests, använder WebApplicationFactory för att bootstrap:a SUT och tillhandahålla en HttpClient till testmetoden Get_EndpointsReturnSuccessAndCorrectContentType. Metoden verifierar att svarsstatuskoden är framgångsrik (200–299) och Content-Type-huvudet är text/html; charset=utf-8 för flera applikationssidor.

CreateClient() skapar en instans av HttpClient som automatiskt följer omdirigeringar och hanterar cookies.

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

Som standard bevaras inte icke-nödvändiga cookies mellan begäranden när princip för medgivande för den allmänna dataskyddsförordningen är aktiverad. Om du vill bevara icke-nödvändiga cookies, till exempel de som används av TempData-providern, markerar du dem som viktiga i dina tester. Anvisningar om hur du markerar ett cookie som viktigt finns i Viktiga cookies.

AngleSharp jämfört med Application Parts för förfalskningskontroller

Den här artikeln använder AngleSharp parser för att hantera kontroller mot förfalskning genom att läsa in sidor och parsa HTML. Om du vill testa slutpunkterna för kontrollant- och Razor Pages-vyer på en lägre nivå, utan att bry dig om hur de återges i webbläsaren, bör du överväga att använda Application Parts. Metoden Application Parts infogar en kontroller eller Razor-sida i appen som kan användas för att göra JSON-begäranden för att hämta de värden som krävs. Mer information finns i blogginlägget Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts och och det tillhörande GitHub-repot av Martin Costello.

Anpassa WebApplicationFactory

Webbhotellkonfiguration kan skapas oberoende av testklasserna genom att ärva från WebApplicationFactory<TEntryPoint> för att skapa en eller flera anpassade fabriker.

  1. Ärv från WebApplicationFactory och åsidosätt ConfigureWebHost. Med IWebHostBuilder kan du konfigurera tjänstsamlingen med 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");
        }
    }
    

    Databasutsöndring i exempelappen utförs av metoden InitializeDbForTests. Metoden beskrivs i exemplet med integrationstest: Testa apporganisationen avsnittet.

    SUT:s databaskontext registreras i Program.cs. Testappens builder.ConfigureServices återanrop körs efter att appens Program.cs kod har körts. Om du vill använda en annan databas för testerna än appens databas måste appens databaskontext ersättas i builder.ConfigureServices.

    Exempelappen hittar tjänstbeskrivningen för databaskontexten och använder beskrivningen för att ta bort tjänstregistreringen. Fabriken lägger sedan till en ny ApplicationDbContext som använder en minnesintern databas för testerna..

    Om du vill ansluta till en annan databas ändrar du DbConnection. Så här använder du en SQL Server-testdatabas:

  1. Använd det anpassade elementet CustomWebApplicationFactory i testklasser. I följande exempel används fabriken i klassen IndexPageTests:

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

    Exempelappens klient är konfigurerad för att förhindra att HttpClient följer omdirigeringar. Som beskrivs senare i avsnittet Mock authentication tillåter detta tester att kontrollera resultatet av appens första svar. Det första svaret är en omdirigering i många av dessa tester med ett Location-huvud.

  2. Ett typiskt test använder HttpClient- och hjälpmetoderna för att bearbeta begäran och svaret:

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

Alla POST-begäranden till SUT måste uppfylla den antiforgery-kontroll som automatiskt görs av appens dataskyddsskyddsskyddssystem. För att kunna ordna för en tests POST-begäran måste testappen:

  1. Gör en begäran för sidan.
  2. Parsa antiforgery-cookie och begär valideringstoken från svaret.
  3. Gör POST-begäran med antiforgery cookie och se till att valideringstoken finns.

SendAsync-tilläggsmetoderna (Helpers/HttpClientExtensions.cs) och GetDocumentAsync-hjälpmetoden (Helpers/HtmlHelpers.cs) i exempelappen använder AngleSharp-parser för att hantera förfalskningsskyddet med efterföljande metoder:

  • GetDocumentAsync: Tar emot HttpResponseMessage och returnerar en IHtmlDocument. GetDocumentAsync använder en fabrik som förbereder ett virtuellt svar baserat på den ursprungliga HttpResponseMessage. Mer information finns i AngleSharp-dokumentationen.
  • SendAsync tilläggsmetoder för HttpClient för att skapa en HttpRequestMessage och anropa SendAsync(HttpRequestMessage) för att skicka begäranden till SUT. Överbelastningar för SendAsync accepterar HTML-form (IHtmlFormElement) och följande:
    • Knappen Skicka i formuläret (IHtmlElement)
    • Samling med formulärvärden (IEnumerable<KeyValuePair<string, string>>)
    • Knappen Skicka (IHtmlElement) och formulärvärden (IEnumerable<KeyValuePair<string, string>>)

AngleSharp är ett tolkningsbibliotek från tredje part som används för demonstration i den här artikeln och exempelprogrammet. AngleSharp stöds inte eller krävs inte för integreringstestning av ASP.NET Core-appar. Andra parsers kan användas, till exempel HTML Agility Pack (HAP). En annan metod är att skriva kod för att direkt hantera antiforgery-systemets begäran om verifieringstoken och antiforgery cookie. Mer information finns om AngleSharp vs Application Parts för förfalskningskontroller i den här artikeln.

Den EF-Core minnesinterna databasprovidern kan användas för begränsad och grundläggande testning, men SQLite-providern är det rekommenderade valet för minnesintern testning.

Se Utöka start med startfilter som visar hur du konfigurerar mellanprogram med IStartupFilter, vilket är användbart när ett test kräver en anpassad tjänst eller mellanprogram.

Anpassa klienten med WithWebHostBuilder

När ytterligare konfiguration krävs inom en testmetod skapar WithWebHostBuilder en ny WebApplicationFactory med en IWebHostBuilder som anpassas ytterligare efter konfiguration.

-exempelkoden anropar WithWebHostBuilder för att ersätta konfigurerade tjänster med teststubbar. För mer information och exempel på användning, se Infoga mocktjänster i den här artikeln.

Testmetoden Post_DeleteMessageHandler_ReturnsRedirectToRoot för exempelappen visar användningen av WithWebHostBuilder. Det här testet utför en radering av post i databasen genom att utlösa en formulärinlämning i SUT.

Eftersom ett annat test i klassen IndexPageTests utför en åtgärd som tar bort alla poster i databasen och kan köras före metoden Post_DeleteMessageHandler_ReturnsRedirectToRoot, återställs databasen i den här testmetoden för att säkerställa att det finns en post för SUT att ta bort. Om du väljer den första borttagningsknappen i formuläret messages i SUT, skickas simuleringen som en begäran till 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);
}

Klientalternativ

Se sidan WebApplicationFactoryClientOptions för standardvärden och tillgängliga alternativ när du skapar HttpClient instanser.

Skapa klassen WebApplicationFactoryClientOptions och skicka den till metoden CreateClient():

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

OBS! Om du vill undvika HTTPS-omdirigeringsvarningar i loggar när du använder HTTPS Redirection Middleware anger du BaseAddress = new Uri("https://localhost")

Infoga mocktjänster

Tjänster kan åsidosättas i ett test med ett anrop till ConfigureTestServices på värdbyggaren. För att avgränsa de åsidosatta tjänsterna till själva testet används WithWebHostBuilder-metoden för att hämta en host builder. Detta kan visas i följande tester:

Exempel-SUT innehåller en tjänst med avgränsat område som returnerar ett citat. Citatet bäddas in i ett dolt fält på sidan Index när den begärs.

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

Följande markering genereras när SUT-appen körs:

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

För att testa tjänsten och offertinmatningen i ett integreringstest matas en modelltjänst in i SUT:en av testet. Mock-tjänsten ersätter appens QuoteService med en tjänst som tillhandahålls av testappen med namnet 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 anropas och den begränsade tjänsten registreras:

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

Markupen som producerades under testets körning återspeglar citattexten som tillhandahålls av TestQuoteService, vilket innebär att kontrollen klarar:

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

Simulera autentisering

Tester i klassen AuthTests verifierar att en säker endpunkt:

  • Omdirigerar en oautentiserad användare till appens inloggningssida.
  • Returnerar innehåll för en autentiserad användare.

I SUT används /SecurePage-sidan en AuthorizePage-konvention för att applicera en AuthorizeFilter på sidan. Mer information finns i Razor Pages-auktoriseringskonventioner.

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

I det Get_SecurePageRedirectsAnUnauthenticatedUser testet är en WebApplicationFactoryClientOptions inställd på att förhindra omdirigeringar genom att ange AllowAutoRedirect till false:

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

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

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

Genom att inte tillåta att klienten följer omdirigeringen kan följande kontroller göras:

  • Statuskoden som returneras av SUT kan kontrolleras mot det förväntade HttpStatusCode.Redirect resultatet, inte den slutliga statuskoden efter omdirigeringen till inloggningssidan, vilket skulle vara HttpStatusCode.OK.
  • Det Location sidhuvudvärdet i svarshuvudena kontrolleras för att bekräfta att det börjar med http://localhost/Identity/Account/Login, inte det slutliga svaret på inloggningssidan, där Location-huvudet inte skulle vara närvarande.

Testappen kan simulera en AuthenticationHandler<TOptions> i ConfigureTestServices för att testa aspekter av autentisering och auktorisering. Ett minimalt scenario returnerar en 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);
    }
}

TestAuthHandler anropas för att autentisera en användare när autentiseringsschemat är inställt på TestScheme där AddAuthentication har registrerats för ConfigureTestServices. Det är viktigt att TestScheme-schemat matchar det schema som din app förväntar sig. Annars fungerar inte autentiseringen.

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

Mer information om WebApplicationFactoryClientOptionsfinns i avsnittet Klientalternativ.

Grundläggande tester för mellanprogram för autentisering

Se den här GitHub-lagringsplatsen för grundläggande tester av mellanprogram för autentisering. Den innehåller en testserver som är specifik för testscenariot.

Ange miljön

Ange miljö i den anpassade programfabriken:

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

Hur testinfrastrukturen fastställer appens innehållsrotväg

WebApplicationFactory-konstruktorn härleder sökvägen till appens innehållsrot genom att söka efter en WebApplicationFactoryContentRootAttribute i assemblyn som innehåller integrationstesterna vars nyckel är lika med TEntryPoint assembly System.Reflection.Assembly.FullName. Om ett attribut med rätt nyckel inte hittas söker WebApplicationFactory efter en lösningsfil (.sln) och lägger till sammansättningsnamnet TEntryPoint i lösningens katalog. Appens rotkatalog (innehållsrotsökvägen) används för att identifiera vyer och innehållsfiler.

Inaktivera skuggkopiering

Skuggkopiering gör att testerna körs i en annan katalog än utdatakatalogen. Om dina tester förlitar sig på att läsa in filer i förhållande till Assembly.Location och du stöter på problem kan du behöva inaktivera skuggkopiering.

Om du vill inaktivera skuggkopiering när du använder xUnit skapar du en xunit.runner.json fil i testprojektkatalogen med rätt konfigurationsinställning:

{
  "shadowCopy": false
}

Bortskaffande av objekt

När testerna av IClassFixture-implementeringen har körts tas TestServer och HttpClient bort när xUnit tar bort WebApplicationFactory. Om objekt som instansieras av utvecklaren kräver bortskaffande ska du ta bort dem i IClassFixture implementeringen. Mer information finns i Implementera en dispose-metod.

Exempel på integreringstester

Den exempelappen består av två appar:

Applikation Projektkatalog Beskrivning
Meddelandeapp (SUT) src/RazorPagesProject Tillåter att en användare lägger till, tar bort en, tar bort alla och analyserar meddelanden.
Testapp tests/RazorPagesProject.Tests Används för att integrationstesta SUT.

Testerna kan köras med hjälp av de inbyggda testfunktionerna i en IDE, till exempel Visual Studio. Om du använder Visual Studio Code eller kommandoraden, kör du följande kommando vid en kommandoprompt i katalogen tests/RazorPagesProject.Tests:

dotnet test

Meddelandeappsorganisation (SUT)

SUT är ett meddelandesystem för Razor Pages med följande egenskaper:

  • Sidan Index i appen (Pages/Index.cshtml och Pages/Index.cshtml.cs) innehåller ett användargränssnitt och sidmodellmetoder för att styra tillägg, borttagning och analys av meddelanden (genomsnittliga ord per meddelande).
  • Ett meddelande beskrivs av klassen Message (Data/Message.cs) med två egenskaper: Id (nyckel) och Text (meddelande). Egenskapen Text krävs och är begränsad till 200 tecken.
  • Meddelanden lagras med hjälp av Entity Frameworks minnesinterna databas†.
  • Appen innehåller ett dataåtkomstlager (DAL) i sin databaskontextklass AppDbContext (Data/AppDbContext.cs).
  • Om databasen är tom vid appstart initieras meddelandearkivet med tre meddelanden.
  • Appen innehåller en /SecurePage som bara kan nås av en autentiserad användare.

† EF-artikeln, Test with InMemory, förklarar hur du använder en minnesintern databas för tester med MSTest. Det här avsnittet använder testramverket xUnit. Testbegrepp och testimplementeringar i olika testramverk är liknande men inte identiska.

Även om appen inte använder lagringsplatsens mönster och inte är ett effektivt exempel på UoW-mönstret (Unit of Work), stöder Razor Pages dessa utvecklingsmönster. Mer information finns i Utforma beständighetslager för infrastruktur och Testa styrenhetslogik (exemplet implementerar förvaringsmönstret).

Testapporganisation

Testappen är en konsolapp i katalogen tests/RazorPagesProject.Tests.

Testappregister Beskrivning
AuthTests Innehåller testmetoder för:
  • Åtkomst till en säker sida av en oautentiserad användare.
  • Åtkomst till en säker sida av en autentiserad användare med en falsk AuthenticationHandler<TOptions>.
  • Hämta en GitHub-användarprofil och kontrollera profilens användarinloggning.
BasicTests Innehåller en testmetod för routning och innehållstyp.
IntegrationTests Innehåller integreringstesterna för sidan Index med anpassad WebApplicationFactory-klass.
Helpers/Utilities
  • Utilities.cs innehåller den InitializeDbForTests metod som används för att seeda databasen med testdata.
  • HtmlHelpers.cs tillhandahåller en metod för att returnera en AngleSharp IHtmlDocument för användning i testmetoderna.
  • HttpClientExtensions.cs tillhandahåller överbelastningar för SendAsync att skicka förfrågningar till SUT.

Testramverket är xUnit. Integreringstester utförs med hjälp av Microsoft.AspNetCore.TestHost, som innehåller TestServer. Eftersom Microsoft.AspNetCore.Mvc.Testing-paketet används för att konfigurera testvärden och testservern behöver TestHost- och TestServer-paketen inte direkta paketreferenser i testappens projektfil eller utvecklarkonfiguration i testappen.

Integreringstester kräver vanligtvis en liten datauppsättning i databasen före testkörningen. Ett borttagningstest anropar till exempel en borttagning av en databaspost, så databasen måste ha minst en post för att borttagningsbegäran ska lyckas.

Exempelappen initierar databasen med tre meddelanden i Utilities.cs, som testerna kan använda när de utförs:

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

SUT:s databaskontext registreras i Program.cs. Testappens builder.ConfigureServices återanrop körs efter att appens Program.cs kod har körts. Om du vill använda en annan databas för testerna måste appens databaskontext ersättas i builder.ConfigureServices. Mer information finns i avsnittet Anpassa WebApplicationFactory.

Ytterligare resurser

Den här artikeln förutsätter en grundläggande förståelse av enhetstester. Om du inte är bekant med testbegrepp kan du läsa artikeln Enhetstestning i .NET Core och .NET Standard och dess länkade innehåll.

Visa eller ladda ned exempelkod (hur du laddar ned)

Exempelappen är en Razor Pages-app och förutsätter en grundläggande förståelse för Razor Pages. Om du inte är bekant med Razor Pages kan du läsa följande artiklar:

För att testa SPA:errekommenderar vi ett verktyg som Playwright för .NET, som kan automatisera en webbläsare.

Introduktion till integreringstester

Integreringstester utvärderar en apps komponenter på en bredare nivå än enhetstester. Enhetstester används för att testa isolerade programvarukomponenter, till exempel enskilda klassmetoder. Integreringstester bekräftar att två eller flera appkomponenter fungerar tillsammans för att skapa ett förväntat resultat, eventuellt inklusive varje komponent som krävs för att bearbeta en begäran fullt ut.

Dessa bredare tester används för att testa appens infrastruktur och hela ramverket, ofta med följande komponenter:

  • Databas
  • Filsystem
  • Nätverksinstallationer
  • Pipeline för begärandesvar

Enhetstester använder fabricerade komponenter, så kallade falska eller falska objekt, i stället för infrastrukturkomponenter.

Till skillnad från enhetstester, integreringstester:

  • Använd de faktiska komponenter som appen använder i produktion.
  • Kräv mer kod och databearbetning.
  • Tar längre tid att genomföra.

Begränsa därför användningen av integreringstester till de viktigaste infrastrukturscenarierna. Om ett beteende kan testas med antingen ett enhetstest eller ett integreringstest väljer du enhetstestet.

I diskussioner om integreringstester kallas det testade projektet ofta System Under Test, eller "SUT" för kort. "SUT" används i hela den här artikeln för att referera till ASP.NET Core-appen som testas.

Skriv inte integreringstester för varje permutation av data och filåtkomst med databaser och filsystem. Oavsett hur många platser i en app som interagerar med databaser och filsystem kan en prioriterad uppsättning av integreringstester för läsning, skrivning, uppdatering och borttagning vanligtvis testa databas- och filsystemkomponenter på ett tillfredsställande sätt. Använd enhetstester för rutinmässiga tester av metodlogik som interagerar med dessa komponenter. I enhetstester resulterar användningen av infrastruktur-förfalskningar eller mockar till snabbare testkörning.

ASP.NET Core-integreringstester

Integreringstester i ASP.NET Core kräver följande:

  • Ett testprojekt används för att innehålla och köra testerna. Testprojektet har en referens till SUT.
  • Testprojektet skapar en testwebbvärd för SUT och använder en testserverklient för att hantera begäranden och svar med SUT.
  • En testkörare används för att köra testerna och rapportera testresultaten.

Integreringstester följer en sekvens med händelser som innehåller de vanliga teststegen Ordna, Utföraoch Bekräfta:

  1. SUT:s webbhotell är konfigurerat.
  2. En testserverklient skapas för att skicka begäranden till appen.
  3. Teststeget Ordna körs: Testappen förbereder en begäran.
  4. Teststeget Act körs: Klienten skickar begäran och tar emot svaret.
  5. Teststeget Assert körs: Det faktiska-svaret verifieras som ett godkänt eller underkänt baserat på ett förväntat svar.
  6. Processen fortsätter tills alla tester körs.
  7. Testresultaten rapporteras.

Vanligtvis är testwebbvärden konfigurerad annorlunda än appens normala webbvärd under testkörningar. Till exempel kan en annan databas eller olika appinställningar användas för testerna.

Infrastrukturkomponenter, till exempel testwebbvärden och minnesintern testserver (TestServer), tillhandahålls eller hanteras av Microsoft.AspNetCore.Mvc.Testing-paketet. Användning av det här paketet effektiviserar skapande och körning av test.

Microsoft.AspNetCore.Mvc.Testing-paketet hanterar följande uppgifter:

  • Kopierar beroendefilen (.deps) från SUT till testprojektets bin katalog.
  • Anger innehållsroten till SUT:s projektrot så att statiska filer och sidor/vyer hittas när testerna körs.
  • Tillhandahåller klassen WebApplicationFactory för att effektivisera initieringen av SUT med TestServer.

I enhetstester dokumentationen beskrivs hur du konfigurerar ett testprojekt och en testlöpare, tillsammans med detaljerade instruktioner om hur du kör tester och rekommendationer för hur du namnger tester och testklasser.

Separera enhetstester från integreringstester i olika projekt. Att separera testerna:

  • Hjälper till att säkerställa att komponenter för infrastrukturtestning inte oavsiktligt ingår i enhetstesterna.
  • Tillåter kontroll över vilken uppsättning tester som körs.

Det finns praktiskt taget ingen skillnad mellan konfigurationen för tester av Razor Pages-appar och MVC-appar. Den enda skillnaden är hur testerna namnges. I en Razor Pages-app namnges vanligtvis tester av sidslutpunkter efter sidmodellklassen (till exempel IndexPageTests för att testa komponentintegrering för indexsidan). I en MVC-app ordnas tester vanligtvis efter kontrollantklasser och namnges efter de kontrollanter som de testar (till exempel HomeControllerTests för att testa komponentintegrering för Home kontrollanten).

Krav för testapp

Testprojektet måste:

Dessa förutsättningar kan ses i exempelappen. tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj Granska filen. Exempelappen använder testramverket xUnit och AngleSharp parser-biblioteket, så exempelappen refererar också till:

I appar som använder xunit.runner.visualstudio version 2.4.2 eller senare måste testprojektet referera till Microsoft.NET.Test.Sdk-paketet.

Entity Framework Core används också i testerna. Se -projektfilen på GitHub.

SUT-miljö

Om SUT:s miljö inte är satt, faller miljön tillbaka till Utveckling.

Grundläggande tester med standard-WebApplicationFactory

Exponera den implicit definierade Program-klassen för testprojektet genom att göra något av följande:

  • Exponera interna typer från webbappen till testprojektet. Detta kan göras i SUT-projektets fil (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Gör Program-klassen offentlig med hjälp av en partiell klass-deklaration:

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

    Den exempelappen använder den Program partiella klassmetoden.

WebApplicationFactory<TEntryPoint> används för att skapa en TestServer för integreringstesterna. TEntryPoint är startpunktsklassen för SUT, vanligtvis Program.cs.

Testklasser implementerar ett klassfixtur gränssnitt (IClassFixture) för att indikera att klassen innehåller tester och tillhandahåller delade objektinstanser i testerna i klassen.

Följande testklass, BasicTests, använder WebApplicationFactory för att initiera SUT och tillhandahålla ett HttpClient till en testmetod, Get_EndpointsReturnSuccessAndCorrectContentType. Metoden verifierar att svarsstatuskoden är framgångsrik (200–299) och att Content-Type-huvudet är text/html; charset=utf-8 för flera appsidor.

CreateClient() skapar en instans av HttpClient som automatiskt följer omdirigeringar och hanterar cookies.

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

Som standard bevaras inte icke-nödvändiga cookies mellan begäranden när princip för medgivande för den allmänna dataskyddsförordningen är aktiverad. Om du vill bevara icke-nödvändiga cookies, till exempel de som används av TempData-providern, markerar du dem som viktiga i dina tester. Anvisningar om hur du markerar ett cookie som viktigt finns i Viktiga cookies.

AngleSharp jämfört med Application Parts för förfalskningskontroller

Den här artikeln använder AngleSharp parser för att hantera kontroller mot förfalskning genom att läsa in sidor och parsa HTML. Om du vill testa slutpunkterna för kontrollant- och Razor Pages-vyer på en lägre nivå, utan att bry dig om hur de återges i webbläsaren, bör du överväga att använda Application Parts. Metoden Application Parts infogar en kontroll eller en Razor-sida i appen som kan användas för att göra JSON-begäranden för att hämta de nödvändiga värdena. Mer information finns i bloggen Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts and associated GitHub repo by Martin Costello.

Anpassa WebApplicationFactory

Webbvärdkonfiguration kan skapas oberoende av testklasserna genom att ärva från WebApplicationFactory<TEntryPoint> för att skapa en eller flera anpassade fabriker:

  1. Ärv från WebApplicationFactory och åsidosätt ConfigureWebHost. Med IWebHostBuilder kan du konfigurera tjänstsamlingen med 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");
        }
    }
    

    Databasutsöndring i exempelappen utförs av metoden InitializeDbForTests. Metoden beskrivs i exemplet med integrationstest: Testa apporganisationen avsnittet.

    SUT:s databaskontext registreras i Program.cs. Testappens builder.ConfigureServices återanrop körs efter att appens Program.cs kod har körts. Om du vill använda en annan databas för testerna än appens databas måste appens databaskontext ersättas i builder.ConfigureServices.

    Exempelappen hittar tjänstbeskrivningen för databaskontexten och använder beskrivningen för att ta bort tjänstregistreringen. Fabriken lägger sedan till en ny ApplicationDbContext som använder en minnesintern databas för testerna..

    Om du vill ansluta till en annan databas ändrar du DbConnection. Så här använder du en SQL Server-testdatabas:

  1. Använd den anpassade CustomWebApplicationFactory i testklasser. I följande exempel används fabriken i klassen IndexPageTests:

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

    Exempelappens klient är konfigurerad för att förhindra att HttpClient följer omdirigeringar. Som beskrivs senare i avsnittet Mock authentication tillåter detta tester att kontrollera resultatet av appens första svar. Det första svaret är en omdirigering i många av dessa tester med ett Location-huvud.

  2. Ett typiskt test använder HttpClient- och hjälpmetoderna för att bearbeta begäran och svaret:

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

Alla POST-begäranden till SUT måste uppfylla den antiförfalskningskontroll som automatiskt görs av appens dataskyddssystem. För att kunna förbereda en POST-begäran för testet måste testappen:

  1. Gör en begäran för sidan.
  2. Analysera antiforgery-cookie och begär validerings-token från svaret.
  3. Gör POST-begäran med antiforgery-cookie och begär valideringstoken på plats.

SendAsync-tilläggsmetoderna (Helpers/HttpClientExtensions.cs) och GetDocumentAsync-hjälpmetoden (Helpers/HtmlHelpers.cs) i exempelappen använder AngleSharp parser för att hantera antiforfalskningskontrollen med följande metoder:

  • GetDocumentAsync: Tar emot HttpResponseMessage och returnerar en IHtmlDocument. GetDocumentAsync använder en fabrik som förbereder ett virtuellt svar baserat på den ursprungliga HttpResponseMessage. Mer information finns i AngleSharp-dokumentationen.
  • SendAsync tilläggsmetoder för HttpClient skapa en HttpRequestMessage och anropa SendAsync(HttpRequestMessage) för att skicka begäranden till SUT. överlagringar för SendAsync accepterar HTML-formuläret (IHtmlFormElement) och följande:
    • Knappen Skicka i formuläret (IHtmlElement)
    • Samling med formulärvärden (IEnumerable<KeyValuePair<string, string>>)
    • Knappen Skicka (IHtmlElement) och formulärvärden (IEnumerable<KeyValuePair<string, string>>)

AngleSharp är ett bibliotek från tredje part som används för demonstrationssyften i den här artikeln och exempelappen. AngleSharp stöds inte eller krävs inte för integreringstestning av ASP.NET Core-appar. Andra parsers kan användas, till exempel HTML Agility Pack (HAP). En annan metod är att skriva kod för att direkt hantera verifieringstoken för begäran från antiforgery-systemet och antiforgery-cookie. För mer information om förfalskningskontroller, se AngleSharp vs Application Parts i denna artikel.

Den EF-Core minnesinterna databasprovidern kan användas för begränsad och grundläggande testning, men SQLite-providern är det rekommenderade valet för minnesintern testning.

Se Utöka start med startfilter som visar hur du konfigurerar mellanprogram med IStartupFilter, vilket är användbart när ett test kräver en anpassad tjänst eller mellanprogram.

Anpassa klienten med WithWebHostBuilder

När ytterligare konfiguration krävs inom en testmetod skapar WithWebHostBuilder en ny WebApplicationFactory med en IWebHostBuilder som anpassas ytterligare efter konfiguration.

-exempelkoden anropar WithWebHostBuilder för att ersätta konfigurerade tjänster med teststubbar. Mer information och exempel på användning finns i Mata in falska tjänster i den här artikeln.

Den Post_DeleteMessageHandler_ReturnsRedirectToRoot testmetoden för -exempelappen demonstrerar hur man använder WithWebHostBuilder. Det här testet tar bort en post i databasen genom att skicka in ett formulär i SUT.

Eftersom ett annat test i klassen IndexPageTests utför en åtgärd som tar bort alla poster i databasen och kan köras före metoden Post_DeleteMessageHandler_ReturnsRedirectToRoot, återställs databasen i den här testmetoden för att säkerställa att det finns en post för SUT att ta bort. Genom att välja den första radera-knappen i formuläret messages i SUT simuleras åtgärden i begäran till 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);
}

Klientalternativ

Se sidan WebApplicationFactoryClientOptions för standardvärden och tillgängliga alternativ när du skapar HttpClient instanser.

Skapa klassen WebApplicationFactoryClientOptions och skicka den till metoden CreateClient():

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

OBS! Om du vill undvika HTTPS-omdirigeringsvarningar i loggar när du använder HTTPS Redirection Middleware anger du BaseAddress = new Uri("https://localhost")

Mata in falska tjänster

Tjänster kan åsidosättas i ett test med ett anrop till ConfigureTestServices på värdbyggaren. Om du vill begränsa de åsidosatta tjänsterna till själva testet används WithWebHostBuilder-metoden för att hämta en host builder. Detta kan visas i följande tester:

Exempel-SUT innehåller en avgränsad tjänst som returnerar ett citat. Citatet bäddas in i ett dolt fält på Indexsidan när den begärs.

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

Följande markering genereras när SUT-appen körs:

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

För att testa tjänsten och offertinmatningen i ett integreringstest matas en modelltjänst in i SUT:en av testet. Mock-tjänsten ersätter appens QuoteService med en tjänst som tillhandahålls av testappen med namnet 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 anropas, och den avgränsade tjänsten registreras.

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

Markeringen som producerades under testets körning återspeglar citattexten som tillhandahålls av TestQuoteService, vilket innebär att påståendet går igenom.

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

Simulera autentisering

Tester i klassen AuthTests kontrollerar att en säker slutpunkt:

  • Omdirigerar en oautentiserad användare till appens inloggningssida.
  • Returnerar innehåll för en autentiserad användare.

I SUT används /SecurePage-sidan en AuthorizePage-konvention för att tillämpa en AuthorizeFilter på sidan. Mer information finns i Razor Pages-auktoriseringskonventioner.

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

I det Get_SecurePageRedirectsAnUnauthenticatedUser-testet är en WebApplicationFactoryClientOptions inställd på att förhindra omdirigeringar genom att ange AllowAutoRedirect till false:

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

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

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

Genom att inte tillåta att klienten följer omdirigeringen kan följande kontroller göras:

  • Statuskoden som returneras av SUT kan kontrolleras mot det förväntade HttpStatusCode.Redirect resultatet, inte den slutliga statuskoden efter omdirigeringen till inloggningssidan, vilket skulle vara HttpStatusCode.OK.
  • Det Location-värdet i svarshuvudena kontrolleras för att bekräfta att det börjar med http://localhost/Identity/Account/Login, inte det slutliga inloggningssvarssidan, där Location-huvudet inte skulle finnas.

Testappen kan simulera en AuthenticationHandler<TOptions> i ConfigureTestServices för att testa aspekter av autentisering och auktorisering. Ett minimalt scenario returnerar en 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);
    }
}

TestAuthHandler anropas för att autentisera en användare när autentiseringsschemat är inställt på TestScheme där AddAuthentication har registrerats för ConfigureTestServices. Det är viktigt att TestScheme-schemat matchar det schema som din app förväntar sig. Annars fungerar inte autentiseringen.

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

Mer information om WebApplicationFactoryClientOptionsfinns i avsnittet Klientalternativ.

Grundläggande tester för mellanprogram för autentisering

Se den här GitHub-lagringsplatsen för grundläggande tester av mellanprogram för autentisering. Den innehåller en testserver som är specifik för testscenariot.

Ange miljön

Ange miljö i den anpassade programfabriken:

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

Hur testinfrastrukturen fastställer appens innehållsrotväg

WebApplicationFactory-konstruktorn härleder appens innehållsrots sökväg genom att söka efter en WebApplicationFactoryContentRootAttribute i den assembly som innehåller integrationstesterna, med en nyckel som är lika med TEntryPoint assembly System.Reflection.Assembly.FullName. Om ett attribut med rätt nyckel inte hittas återgår WebApplicationFactory till att söka efter en lösningsfil (.sln) och lägger till TEntryPoint assembly name i lösningsmappen. Appens rotkatalog (innehållsrotsökvägen) används för att identifiera vyer och innehållsfiler.

Inaktivera skuggkopiering

Skuggkopiering gör att testerna körs i en annan katalog än utdatakatalogen. Om dina tester förlitar sig på att läsa in filer i förhållande till Assembly.Location och du stöter på problem kan du behöva inaktivera skuggkopiering.

Om du vill inaktivera skuggkopiering när du använder xUnit skapar du en xunit.runner.json fil i testprojektkatalogen med rätt konfigurationsinställning:

{
  "shadowCopy": false
}

Bortskaffande av objekt

När testerna av IClassFixture-implementeringen har körts tas TestServer och HttpClient bort när xUnit tar bort WebApplicationFactory. Om objekt som instansieras av utvecklaren kräver bortskaffande ska du ta bort dem i IClassFixture implementeringen. Mer information finns i Implementera en dispose-metod.

Exempel på integreringstester

Den exempelappen består av två appar:

Applikation Projektkatalog Beskrivning
Meddelandeapp (SUT) src/RazorPagesProject Tillåter att en användare lägger till, tar bort en, tar bort alla och analyserar meddelanden.
Testapp tests/RazorPagesProject.Tests Används för att utföra integrationstest av SUT.

Testerna kan köras med hjälp av de inbyggda testfunktionerna i en IDE, till exempel Visual Studio. Om du använder Visual Studio Code- eller kommandoraden kör du följande kommando i en kommandotolk i katalogen tests/RazorPagesProject.Tests:

dotnet test

Meddelandeappsorganisation (SUT)

SUT är ett meddelandesystem för Razor Pages med följande egenskaper:

  • Sidan Index i appen (Pages/Index.cshtml och Pages/Index.cshtml.cs) innehåller ett användargränssnitt och sidmodellmetoder för att styra tillägg, borttagning och analys av meddelanden (genomsnittliga ord per meddelande).
  • Ett meddelande beskrivs av klassen Message (Data/Message.cs) med två egenskaper: Id (nyckel) och Text (meddelande). Egenskapen Text krävs och är begränsad till 200 tecken.
  • Meddelanden lagras med hjälp av Entity Frameworks minnesinterna databas†.
  • Appen innehåller ett dataåtkomstlager (DAL) i sin databaskontextklass AppDbContext (Data/AppDbContext.cs).
  • Om databasen är tom vid appstart initieras meddelandearkivet med tre meddelanden.
  • Appen innehåller en /SecurePage som bara kan nås av en autentiserad användare.

† EF-artikeln, Test with InMemory, förklarar hur du använder en minnesintern databas för tester med MSTest. Det här avsnittet använder testramverket xUnit. Testbegrepp och testimplementeringar i olika testramverk är liknande men inte identiska.

Även om appen inte använder lagringsplatsens mönster och inte är ett effektivt exempel på UoW-mönstret (Unit of Work), stöder Razor Pages dessa utvecklingsmönster. Mer information finns i Utforma den infrastrukturella beständighetsskiktet och testkontrollerlogik (exemplet implementerar lagringsmönstret).

Testapporganisation

Testappen är en konsolapp i katalogen tests/RazorPagesProject.Tests.

Testappkatalog Beskrivning
AuthTests Innehåller testmetoder för:
  • Åtkomst till en säker sida av en oautentiserad användare.
  • Åtkomst till en säker sida av en autentiserad användare med en falsk AuthenticationHandler<TOptions>.
  • Hämta en GitHub-användarprofil och kontrollera profilens användarinloggning.
BasicTests Innehåller en testmetod för routning och innehållstyp.
IntegrationTests Innehåller integreringstesterna för sidan Index med anpassad WebApplicationFactory-klass.
Helpers/Utilities
  • Utilities.cs innehåller den InitializeDbForTests metod som används för att seeda databasen med testdata.
  • HtmlHelpers.cs tillhandahåller en metod för att returnera en AngleSharp-IHtmlDocument som kan användas av testmetoder.
  • HttpClientExtensions.cs tillhandahålla överladdningar för SendAsync att skicka in begäranden till SUT.

Testramverket är xUnit. Integreringstester utförs med hjälp av Microsoft.AspNetCore.TestHost, som innehåller TestServer. Eftersom Microsoft.AspNetCore.Mvc.Testing-paketet används för att konfigurera testvärden och testservern behöver TestHost- och TestServer-paketen inte ha direkta paketreferenser i testappens projektfil eller utvecklarkonfiguration i testappen.

Integreringstester kräver vanligtvis en liten datauppsättning i databasen före testkörningen. Ett borttagningstest anropar till exempel en borttagning av en databaspost, så databasen måste ha minst en post för att borttagningsbegäran ska lyckas.

Exempelappen förser databasen med hjälp av tre meddelanden i Utilities.cs som testerna kan använda när de körs.

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

SUT:s databaskontext registreras i Program.cs. Testappens builder.ConfigureServices återkopplingsfunktion körs efter att appens Program.cs kod har körts. Om du vill använda en annan databas för testerna måste appens databaskontext ersättas i builder.ConfigureServices. Mer information finns i avsnittet Anpassa WebApplicationFactory.

Ytterligare resurser

Den här artikeln förutsätter en grundläggande förståelse av enhetstester. Om du inte är bekant med testbegrepp kan du läsa artikeln Enhetstestning i .NET Core och .NET Standard och dess länkade innehåll.

Visa eller ladda ned exempelkod (hur du laddar ned)

Exempelappen är en Razor Pages-app och förutsätter en grundläggande förståelse för Razor Pages. Om du inte är bekant med Razor Pages kan du läsa följande artiklar:

För att testa SPA:errekommenderar vi ett verktyg som Playwright för .NET, som kan automatisera en webbläsare.

Introduktion till integreringstester

Integreringstester utvärderar en apps komponenter på en bredare nivå än enhetstester. Enhetstester används för att testa isolerade programvarukomponenter, till exempel enskilda klassmetoder. Integreringstester bekräftar att två eller flera appkomponenter fungerar tillsammans för att skapa ett förväntat resultat, eventuellt inklusive varje komponent som krävs för att bearbeta en begäran fullt ut.

Dessa bredare tester används för att testa appens infrastruktur och hela ramverket, ofta med följande komponenter:

  • Databas
  • Filsystem
  • Nätverksinstallationer
  • Pipeline för begärandesvar

Enhetstester använder fabricerade komponenter, så kallade fejkade eller mock-objekt, i stället för infrastrukturkomponenter.

Till skillnad från enhetstester, integreringstester:

  • Använd de faktiska komponenter som appen använder i produktion.
  • Kräv mer kod och databearbetning.
  • Tar längre tid att köra.

Begränsa därför användningen av integreringstester till de viktigaste infrastrukturscenarierna. Om ett beteende kan testas med antingen ett enhetstest eller ett integreringstest väljer du enhetstestet.

I diskussioner om integreringstester kallas det testade projektet ofta System Under Test, eller "SUT" för kort. "SUT" används i hela den här artikeln för att referera till ASP.NET Core-appen som testas.

Skriv inte integreringstester för varje permutation av data och filåtkomst med databaser och filsystem. Oavsett hur många platser i en app som interagerar med databaser och filsystem kan en prioriterad uppsättning av integreringstester för läsning, skrivning, uppdatering och borttagning vanligtvis testa databas- och filsystemkomponenter på ett tillfredsställande sätt. Använd enhetstester för rutinmässiga tester av metodlogik som interagerar med dessa komponenter. I enhetstester resulterar användningen av förfalskningar eller hån i infrastrukturen i snabbare testkörning.

ASP.NET Core-integreringstester

Integreringstester i ASP.NET Core kräver följande:

  • Ett testprojekt används för att innehålla och köra testerna. Testprojektet har en referens till SUT.
  • Testprojektet skapar en testwebbvärd för SUT och använder en testserverklient för att hantera begäranden och svar med SUT.
  • En testkörare används för att köra testerna och rapportera testresultaten.

Integreringstester följer en sekvens av händelser som innehåller de vanliga teststegen Ordna, Utföraoch Bekräfta:

  1. SUT:s webbhotell är konfigurerat.
  2. En testserverklient skapas för att skicka begäranden till appen.
  3. Teststeget Ordna körs: Testappen förbereder en begäran.
  4. Teststeget Act körs: Klienten skickar begäran och tar emot svaret.
  5. Teststeget Assert körs: Det faktiska-svaret verifieras som ett skicka eller misslyckas baserat på ett förväntat svar.
  6. Processen fortsätter tills alla tester körs.
  7. Testresultaten rapporteras.

Vanligtvis är testwebbservern konfigurerad på ett annat sätt än appens normala webbserver under testkörningarna. Till exempel kan en annan databas eller olika appinställningar användas för testerna.

Infrastrukturkomponenter, till exempel testwebbvärden och minnesintern testserver (TestServer), tillhandahålls eller hanteras av Microsoft.AspNetCore.Mvc.Testing-paketet. Användning av det här paketet effektiviserar skapande och körning av test.

Microsoft.AspNetCore.Mvc.Testing-paketet hanterar följande uppgifter:

  • Kopierar beroendefilen (.deps) från SUT till testprojektets bin katalog.
  • Anger innehållsroten till SUT:s projektrot så att statiska filer och sidor/vyer hittas när testerna körs.
  • Tillhandahåller klassen WebApplicationFactory för att effektivisera uppstarten av SUT med TestServer.

I enhetstester dokumentationen beskrivs hur du konfigurerar ett testprojekt och en testlöpare, tillsammans med detaljerade instruktioner om hur du kör tester och rekommendationer för hur du namnger tester och testklasser.

Separera enhetstester från integreringstester i olika projekt. Att separera testerna:

  • Hjälper till att säkerställa att komponenter för infrastrukturtestning inte oavsiktligt ingår i enhetstesterna.
  • Tillåter kontroll över vilken uppsättning tester som körs.

Det finns praktiskt taget ingen skillnad mellan konfigurationen för tester av Razor Pages-appar och MVC-appar. Den enda skillnaden är hur testerna namnges. I en Razor Pages-app namnges vanligtvis tester av sidslutpunkter efter sidmodellklassen (till exempel IndexPageTests för att testa komponentintegrering för indexsidan). I en MVC-app ordnas tester vanligtvis efter kontrollantklasser och namnges efter de kontrollanter som de testar (till exempel HomeControllerTests för att testa komponentintegrering för Home kontrollanten).

Krav för testapp

Testprojektet måste:

Dessa förutsättningar kan ses i exempelappen. tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj Granska filen. Exempelappen använder testramverket xUnit och AngleSharp parser-biblioteket, så exempelappen refererar också till:

I appar som använder xunit.runner.visualstudio version 2.4.2 eller senare måste testprojektet referera till Microsoft.NET.Test.Sdk-paketet.

Entity Framework Core används också i testerna. Se -projektfilen i GitHub.

SUT-miljö

Om SUT:s miljö inte har angetts, är miljön som standard inställd på utveckling.

Grundläggande tester med standard-WebApplicationFactory

Exponera den implicit definierade Program-klassen för testprojektet genom att göra något av följande:

  • Exponera interna typer från webbappen till testprojektet. Detta kan göras i SUT-projektets fil (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Gör Program-klassen offentlig med hjälp av en partiell klass-deklaration:

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

    Den exempelappen använder Program partiell klassmetod.

WebApplicationFactory<TEntryPoint> används för att skapa en TestServer för integreringstesterna. TEntryPoint är startpunktsklassen för SUT, vanligtvis Program.cs.

Testklasser implementerar ett klassfixtur gränssnitt (IClassFixture) för att indikera att klassen innehåller tester och för att tillhandahålla gemensamma objektinstanser i testerna i klassen.

Följande testklass BasicTestsanvänder WebApplicationFactory för att initialisera SUT och tillhandahålla en HttpClient till en testmetod, Get_EndpointsReturnSuccessAndCorrectContentType. Metoden verifierar att svarsstatuskoden är lyckad (200–299) och Content-Type-huvudet är text/html; charset=utf-8 för flera applikationssidor.

CreateClient() skapar en instans av HttpClient som automatiskt följer omdirigeringar och hanterar cookies.

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

Som standard bevaras inte icke-nödvändiga cookies mellan begäranden när princip för medgivande för den allmänna dataskyddsförordningen är aktiverad. Om du vill bevara icke-nödvändiga cookies, till exempel de som används av TempData-providern, markerar du dem som viktiga i dina tester. Anvisningar om hur du markerar ett cookie som viktigt finns i Viktiga cookies.

AngleSharp jämfört med Application Parts för förfalskningskontroller

Den här artikeln använder AngleSharp parser för att hantera kontroller mot förfalskning genom att läsa in sidor och parsa HTML. Om du vill testa slutpunkterna för kontrollant- och Razor Pages-vyer på en lägre nivå, utan att bry dig om hur de återges i webbläsaren, bör du överväga att använda Application Parts. Metoden application parts integrerar en kontroller eller Razor-sida i appen som kan användas för att göra JSON-begäranden för att hämta nödvändiga värden. Mer information finns i bloggen Integration Testing ASP.NET Core Resources Protected with Antiforgery Using Application Parts and associated GitHub repo by Martin Costello.

Anpassa WebApplicationFactory

Webbvärdkonfiguration kan skapas oberoende av testklasserna genom att ärva från WebApplicationFactory<TEntryPoint> för att skapa en eller flera anpassade fabriker:

  1. Ärv från WebApplicationFactory och åsidosätt ConfigureWebHost. Med IWebHostBuilder kan du konfigurera tjänstsamlingen med 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");
        }
    }
    

    Databasutsöndring i exempelappen utförs av metoden InitializeDbForTests. Metoden beskrivs i integrationstester-samplet: Test av apporganisation-sektionen.

    SUT:s databaskontext registreras i Program.cs. Testappens builder.ConfigureServices återanrop körs efter att appens Program.cs kod har körts. Om du vill använda en annan databas för testerna än appens databas måste appens databaskontext ersättas i builder.ConfigureServices.

    Exempelappen hittar tjänstbeskrivningen för databaskontexten och använder beskrivningen för att ta bort tjänstregistreringen. Fabriken lägger sedan till en ny ApplicationDbContext som använder en minnesintern databas för testerna..

    Om du vill ansluta till en annan databas ändrar du DbConnection. Så här använder du en SQL Server-testdatabas:

  1. Använd den anpassade versionen CustomWebApplicationFactory i testklasserna. I följande exempel används fabriken i klassen IndexPageTests:

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

    Exempelappens klient är konfigurerad för att förhindra att HttpClient följer omdirigeringar. Som beskrivs senare i avsnittet Mock authentication tillåter detta tester att kontrollera resultatet av appens första svar. Det första svaret är en omdirigering i många av dessa tester med ett Location-huvud.

  2. Ett typiskt test använder HttpClient- och hjälpmetoderna för att bearbeta begäran och svaret:

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

Alla POST-begäranden till SUT måste uppfylla den antiforgery-kontroll som automatiskt görs av appens dataskyddsskyddsskyddssystem. För att ordna en POST-begäran för ett test måste testappen:

  1. Gör en begäran för sidan.
  2. Parsa antiforgery-cookie och begär valideringstoken från svaret.
  3. Gör POST-begäran med antiforgery-cookie och begär valideringstoken på plats.

SendAsync-tilläggsmetoderna (Helpers/HttpClientExtensions.cs) och GetDocumentAsync-hjälpmetoden (Helpers/HtmlHelpers.cs) i exempelappen använder AngleSharp parser för att hantera antifalsk kontroll med följande metoder:

  • GetDocumentAsync: Tar emot HttpResponseMessage och returnerar en IHtmlDocument. GetDocumentAsync använder en fabrik som förbereder ett virtuellt svar baserat på den ursprungliga HttpResponseMessage. Mer information finns i AngleSharp-dokumentationen.
  • SendAsync tilläggsmetoder för HttpClient skapa en HttpRequestMessage och anropa SendAsync(HttpRequestMessage) för att skicka begäranden till SUT. Överbelastningar för SendAsync accepterar HTML-formuläret (IHtmlFormElement) och följande:
    • Knappen Skicka i formuläret (IHtmlElement)
    • Samling med formulärvärden (IEnumerable<KeyValuePair<string, string>>)
    • Knappen Skicka (IHtmlElement) och formulärvärden (IEnumerable<KeyValuePair<string, string>>)

AngleSharp är ett bibliotek från tredje part som används för demonstration i den här artikeln och i exempelappen. AngleSharp stöds inte eller krävs inte för integreringstestning av ASP.NET Core-appar. Andra parsers kan användas, till exempel HTML Agility Pack (HAP). En annan metod är att skriva kod för att hantera antiforgery-systemets verifieringstoken för begäran och cookie direkt. För mer information se AngleSharp vs Application Parts för förfalskningskontroller i den här artikeln.

Den EF-Core minnesinterna databasprovidern kan användas för begränsad och grundläggande testning, men SQLite-providern är det rekommenderade valet för minnesintern testning.

Se Utöka start med startfilter som visar hur du konfigurerar mellanprogram med IStartupFilter, vilket är användbart när ett test kräver en anpassad tjänst eller mellanprogram.

Anpassa klienten med WithWebHostBuilder

När ytterligare konfiguration krävs inom en testmetod skapar WithWebHostBuilder en ny WebApplicationFactory med en IWebHostBuilder som anpassas ytterligare efter konfiguration.

-exempelkoden anropar WithWebHostBuilder för att ersätta konfigurerade tjänster med teststubbar. Mer information och exempel på användning finns i Mata in falska tjänster i den här artikeln.

Testmetoden Post_DeleteMessageHandler_ReturnsRedirectToRoot för -exempelappen visar användningen av WithWebHostBuilder. Det här testet utför en borttagning av post i databasen genom att utlösa en formulärskickning i SUT.

Eftersom ett annat test i klassen IndexPageTests utför en åtgärd som tar bort alla poster i databasen och kan köras före metoden Post_DeleteMessageHandler_ReturnsRedirectToRoot, återställs databasen i den här testmetoden för att säkerställa att det finns en post för SUT att ta bort. Att välja den första raderingsknappen i formuläret messages i SUT simuleras i begäran till 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);
}

Klientalternativ

Se sidan WebApplicationFactoryClientOptions för standardvärden och tillgängliga alternativ när du skapar HttpClient instanser.

Skapa klassen WebApplicationFactoryClientOptions och skicka den till metoden CreateClient():

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

OBS! Om du vill undvika HTTPS-omdirigeringsvarningar i loggar när du använder HTTPS Redirection Middleware anger du BaseAddress = new Uri("https://localhost")

Mata in falska tjänster

Tjänster kan åsidosättas i ett test med ett anrop till ConfigureTestServices på värdverktyget. Om du vill begränsa de åsidosatta tjänsterna till själva testet, används metoden WithWebHostBuilder för att hämta en host builder. Detta kan visas i följande tester:

Exempel-SUT innehåller en begränsad tjänst som returnerar en offert. Offerten bäddas in i ett dolt fält på Indexsidan när denna begärs.

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

Följande markering genereras när SUT-appen körs:

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

För att testa tjänsten och offertinmatningen i ett integreringstest matas en modelltjänst in i SUT:en av testet. Mock-tjänsten ersätter appens QuoteService med en tjänst som tillhandahålls av testappen med namnet 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 anropas och den avgränsade tjänsten registreras:

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

Markeringen som producerades under testets körning återspeglar citattexten som anges av TestQuoteService, vilket innebär att testet passerar.

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

Simulera autentisering

Tester i klassen AuthTests kontrollerar att en säker endpunkt:

  • Omdirigerar en oautentiserad användare till appens inloggningssida.
  • Returnerar innehåll för en autentiserad användare.

I SUT-sidans /SecurePage används en AuthorizePage-konvention för att tillämpa en AuthorizeFilter på sidan. Mer information finns i Razor Pages-auktoriseringskonventioner.

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

I det Get_SecurePageRedirectsAnUnauthenticatedUser testet är WebApplicationFactoryClientOptions inställd på att inte tillåta omdirigeringar genom att ställa in AllowAutoRedirect till false.

[Fact]
public async Task Get_SecurePageRedirectsAnUnauthenticatedUser()
{
    // Arrange
    var client = _factory.CreateClient(
        new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false
        });

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

    // Assert
    Assert.Equal(HttpStatusCode.Redirect, response.StatusCode);
    Assert.StartsWith("http://localhost/Identity/Account/Login",
        response.Headers.Location.OriginalString);
}

Genom att inte tillåta att klienten följer omdirigeringen kan följande kontroller göras:

  • Statuskoden som returneras av SUT kan kontrolleras mot det förväntade HttpStatusCode.Redirect resultatet, inte den slutliga statuskoden efter omdirigeringen till inloggningssidan, vilket skulle vara HttpStatusCode.OK.
  • Värdet för Location-huvudet i svarshuvudena kontrolleras för att bekräfta att det börjar med http://localhost/Identity/Account/Login, och inte svaret där inloggningssidan visas slutligen, där Location-headern inte finns.

Testappen kan simulera en AuthenticationHandler<TOptions> i ConfigureTestServices i syfte att testa aspekter av autentisering och auktorisering. Ett minimalt scenario returnerar en 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);
    }
}

TestAuthHandler anropas för att autentisera en användare när autentiseringsschemat är inställt på TestScheme där AddAuthentication har registrerats för ConfigureTestServices. Det är viktigt att TestScheme-schemat matchar det schema som din app förväntar sig. Annars fungerar inte autentiseringen.

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

Mer information om WebApplicationFactoryClientOptionsfinns i avsnittet Klientalternativ.

Grundläggande tester för mellanprogram för autentisering

Se den här GitHub-lagringsplatsen för grundläggande tester av mellanprogram för autentisering. Den innehåller en testserver som är specifik för testscenariot.

Ange miljön

Ange miljö i den anpassade programfabriken:

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

Hur testinfrastrukturen fastställer rotvägen för appens innehåll

WebApplicationFactory konstruktorn härleder appen innehållsrotens sökväg genom att söka efter en WebApplicationFactoryContentRootAttribute på sammansättningen som innehåller integreringstesterna med en nyckel som är lika med TEntryPoint sammansättning System.Reflection.Assembly.FullName. Om ett attribut med rätt nyckel inte hittas återgår WebApplicationFactory till att söka efter en lösningsfil (.sln) och lägger till TEntryPoint sammansättningsnamn i lösningskatalogen. Appens rotkatalog (innehållsrotsökvägen) används för att identifiera vyer och innehållsfiler.

Inaktivera skuggkopiering

Skuggkopiering gör att testerna körs i en annan katalog än utdatakatalogen. Om dina tester förlitar sig på att läsa in filer i förhållande till Assembly.Location och du stöter på problem kan du behöva inaktivera skuggkopiering.

Om du vill inaktivera skuggkopiering när du använder xUnit skapar du en xunit.runner.json fil i testprojektkatalogen med rätt konfigurationsinställning:

{
  "shadowCopy": false
}

Bortskaffande av objekt

När testerna av IClassFixture-implementeringen har körts tas TestServer och HttpClient bort när xUnit tar bort WebApplicationFactory. Om objekt som instansieras av utvecklaren kräver bortskaffande ska du ta bort dem i IClassFixture implementeringen. Mer information finns i Implementera en dispose-metod.

Exempel på integreringstester

Den exempelappen består av två appar:

Applikation Projektkatalog Beskrivning
Meddelandeapp (SUT) src/RazorPagesProject Tillåter att en användare lägger till, tar bort en, tar bort alla och analyserar meddelanden.
Testapp tests/RazorPagesProject.Tests Används för att integrera test av SUT.

Testerna kan köras med hjälp av de inbyggda testfunktionerna i en IDE, till exempel Visual Studio. Om du använder Visual Studio Code- eller kommandoraden kör du följande kommando i en kommandotolk i katalogen tests/RazorPagesProject.Tests:

dotnet test

Meddelandeappsorganisation (SUT)

SUT är ett meddelandesystem för Razor Pages med följande egenskaper:

  • Sidan Index i appen (Pages/Index.cshtml och Pages/Index.cshtml.cs) innehåller ett användargränssnitt och sidmodellmetoder för att styra tillägg, borttagning och analys av meddelanden (genomsnittliga ord per meddelande).
  • Ett meddelande beskrivs av klassen Message (Data/Message.cs) med två egenskaper: Id (nyckel) och Text (meddelande). Egenskapen Text krävs och är begränsad till 200 tecken.
  • Meddelanden lagras med hjälp av Entity Frameworks minnesinterna databas†.
  • Appen innehåller ett dataåtkomstlager (DAL) i sin databaskontextklass AppDbContext (Data/AppDbContext.cs).
  • Om databasen är tom vid appstart initieras meddelandearkivet med tre meddelanden.
  • Appen innehåller en /SecurePage som bara kan nås av en autentiserad användare.

† EF-artikeln, Test with InMemory, förklarar hur du använder en minnesintern databas för tester med MSTest. Det här avsnittet använder testramverket xUnit. Testbegrepp och testimplementeringar i olika testramverk är liknande men inte identiska.

Även om appen inte använder lagringsplatsens mönster och inte är ett effektivt exempel på UoW-mönstret (Unit of Work), stöder Razor Pages dessa utvecklingsmönster. Mer information finns i Utforma infrastrukturens persistenslager och testa styrenhetslogik (exemplet implementerar lagringsmönstret).

Testapplikationsorganisation

Testappen är en konsolapp i katalogen tests/RazorPagesProject.Tests.

Testappkatalog Beskrivning
AuthTests Innehåller testmetoder för:
  • Åtkomst till en säker sida av en oautentiserad användare.
  • Åtkomst till en säker sida av en autentiserad användare med en falsk AuthenticationHandler<TOptions>.
  • Hämta en GitHub-användarprofil och kontrollera profilens användarinloggning.
BasicTests Innehåller en testmetod för routning och innehållstyp.
IntegrationTests Innehåller integreringstesterna för sidan Index med anpassad WebApplicationFactory-klass.
Helpers/Utilities
  • Utilities.cs innehåller den InitializeDbForTests metod som används för att seeda databasen med testdata.
  • HtmlHelpers.cs tillhandahåller en metod för att returnera en AngleSharp-IHtmlDocument för användning av testmetoderna.
  • HttpClientExtensions.cs tillhandahåller överlagringar för SendAsync för att skicka begäranden till SUT.

Testramverket är xUnit. Integreringstester utförs med hjälp av Microsoft.AspNetCore.TestHost, som innehåller TestServer. Eftersom Microsoft.AspNetCore.Mvc.Testing-paketet används för att konfigurera testvärd och testserver behöver TestHost- och TestServer-paketen inte ha direkta paketreferenser i testappens projektfil eller någon utvecklarkonfiguration i testappen.

Integreringstester kräver vanligtvis en liten datauppsättning i databasen före testkörningen. Ett borttagningstest anropar till exempel en borttagning av en databaspost, så databasen måste ha minst en post för att borttagningsbegäran ska lyckas.

Exempelappen fyller databasen med tre meddelanden i Utilities.cs som tester kan använda när de körs:

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

SUT:s databaskontext registreras i Program.cs. Testappens builder.ConfigureServices återanrop körs efter att appens Program.cs kod har körts. Om du vill använda en annan databas för testerna måste appens databaskontext ersättas i builder.ConfigureServices. Mer information finns i avsnittet Anpassa WebApplicationFactory.

Ytterligare resurser