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 känner till testbegrepp kan du läsa artikeln Testning i .NET 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());
    }
}
[TestClass]
public class BasicTests
{
    private static CustomWebApplicationFactory<Program> _factory;

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

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

    [TestMethod]
    [DataRow("/")]
    [DataRow("/Index")]
    [DataRow("/About")]
    [DataRow("/Privacy")]
    [DataRow("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

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

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.AreEqual("text/html; charset=utf-8",
            response.Content.Headers.ContentType.ToString());
    }
}
public class BasicTests
{
    private CustomWebApplicationFactory<Program>
        _factory;

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

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

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

    [Theory]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

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

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.That(response.Content.Headers.ContentType.ToString(), Is.EqualTo("text/html; charset=utf-8"));
    }
}

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

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

    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);
    }
    
    [TestMethod]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
        Assert.AreEqual("/", response.Headers.Location.OriginalString);
    }
    
    [Test]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK));
        Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
        Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/"));
    }
    

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);
}
[TestMethod]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

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

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

    // Assert
    Assert.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
    Assert.AreEqual("/", response.Headers.Location.OriginalString);
}
[Test]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

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

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

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

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

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

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

    private HttpClient _client;
    private CustomWebApplicationFactory<Program>
        _factory;

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

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

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

ConfigureTestServices 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);
}
[TestMethod]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.AreEqual("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}
[Test]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.That(quoteElement.Attributes["value"].Value, Is.EqualTo(
        "Something's interfering with time, Mr. Scarman, " +
        "and time is my business."));
}

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

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

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

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

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

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);
    }
}
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

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

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

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

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

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);
}
[TestMethod]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddAuthentication(defaultScheme: "TestScheme")
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    "TestScheme", options => { });
        });
    })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

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

    // Assert
    Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
[Test]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddAuthentication(defaultScheme: "TestScheme")
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    "TestScheme", options => { });
        });
    })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

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

    // Assert
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}

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

            services.Remove(dbContextDescriptor);

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

            services.Remove(dbConnectionDescriptor);

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

                return connection;
            });

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

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

            services.Remove(dbContextDescriptor);

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

            services.Remove(dbConnectionDescriptor);

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

                return connection;
            });

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

        builder.UseEnvironment("Development");
    }
}

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.

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

När testerna av testklassen har körts TestServer och HttpClient tas bort när NUnit tar bort WebApplicationFactory i TearDown -metoden. Om objekt som instansieras av utvecklaren kräver bortskaffande ska du ta bort dem i TearDown metoden. 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 callback-funktion körs . 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
  • 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. 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.

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

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

  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 exemplen för integrationstester: avsnittet Test för apporganisation.

    SUT:s databaskontext är registrerad i dess Startup.ConfigureServices-metod. Testappens builder.ConfigureServices callback-funktion körs . 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 den skräddarsydda 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 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>>)

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.

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

Mata in falska tjänster

Tjänster kan åsidosättas i ett test med ett anrop till ConfigureTestServices på värdverktyget. 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 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.");
    }
}

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 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 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 autentiserings- och auktoriseringsaspekter. 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 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-ä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 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 är registrerad i dess Startup.ConfigureServices-metod. Testappens builder.ConfigureServices callback-funktion körs . 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
  • 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

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.

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(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 exemplen för integrationstester: avsnittet Test för apporganisation.

    SUT:s databaskontext registreras i Program.cs. Testappens builder.ConfigureServices callback-funktion körs . 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, 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 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 callback-funktion körs . 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
  • 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

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.

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(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 exemplen för integrationstester: avsnittet Test för apporganisation.

    SUT:s databaskontext registreras i Program.cs. Testappens builder.ConfigureServices callback-funktion körs . 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(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 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 callback-funktion körs . 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
  • 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

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.

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

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

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

    [TestMethod]
    [DataRow("/")]
    [DataRow("/Index")]
    [DataRow("/About")]
    [DataRow("/Privacy")]
    [DataRow("/Contact")]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

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

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.AreEqual("text/html; charset=utf-8",
            response.Content.Headers.ContentType.ToString());
    }
}
public class BasicTests
{
    private CustomWebApplicationFactory<Program>
        _factory;

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

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

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

    [Theory]
    public async Task Get_EndpointsReturnSuccessAndCorrectContentType(string url)
    {
        // Arrange
        var client = _factory.CreateClient();

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

        // Assert
        response.EnsureSuccessStatusCode(); // Status Code 200-299
        Assert.That(response.Content.Headers.ContentType.ToString(), Is.EqualTo("text/html; charset=utf-8"));
    }
}

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

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

    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);
    }
    
    [TestMethod]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
        Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
        Assert.AreEqual("/", response.Headers.Location.OriginalString);
    }
    
    [Test]
    public async Task Post_DeleteAllMessagesHandler_ReturnsRedirectToRoot()
    {
        // Arrange
        var defaultPage = await _client.GetAsync("/");
        var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    
        //Act
        var response = await _client.SendAsync(
            (IHtmlFormElement)content.QuerySelector("form[id='messages']"),
            (IHtmlButtonElement)content.QuerySelector("button[id='deleteAllBtn']"));
    
        // Assert
        Assert.That(defaultPage.StatusCode, Is.EqualTo(HttpStatusCode.OK));
        Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.Redirect));
        Assert.That(response.Headers.Location.OriginalString, Is.EqualTo("/"));
    }
    

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);
}
[TestMethod]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

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

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

    // Assert
    Assert.AreEqual(HttpStatusCode.OK, defaultPage.StatusCode);
    Assert.AreEqual(HttpStatusCode.Redirect, response.StatusCode);
    Assert.AreEqual("/", response.Headers.Location.OriginalString);
}
[Test]
public async Task Post_DeleteMessageHandler_ReturnsRedirectToRoot()
{
    // Arrange
    using (var scope = _factory.Services.CreateScope())
    {
        var scopedServices = scope.ServiceProvider;
        var db = scopedServices.GetRequiredService<ApplicationDbContext>();

        Utilities.ReinitializeDbForTests(db);
    }

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

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

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

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

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

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

    private HttpClient _client;
    private CustomWebApplicationFactory<Program>
        _factory;

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

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

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

ConfigureTestServices 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);
}
[TestMethod]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.AreEqual("Something's interfering with time, Mr. Scarman, " +
        "and time is my business.", quoteElement.Attributes["value"].Value);
}
[Test]
public async Task Get_QuoteService_ProvidesQuoteInPage()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
        {
            builder.ConfigureTestServices(services =>
            {
                services.AddScoped<IQuoteService, TestQuoteService>();
            });
        })
        .CreateClient();

    //Act
    var defaultPage = await client.GetAsync("/");
    var content = await HtmlHelpers.GetDocumentAsync(defaultPage);
    var quoteElement = content.QuerySelector("#quote");

    // Assert
    Assert.That(quoteElement.Attributes["value"].Value, Is.EqualTo(
        "Something's interfering with time, Mr. Scarman, " +
        "and time is my business."));
}

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

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

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

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

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

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);
    }
}
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

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

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}
public class TestAuthHandler : AuthenticationHandler<AuthenticationSchemeOptions>
{
    public TestAuthHandler(IOptionsMonitor<AuthenticationSchemeOptions> options,
        ILoggerFactory logger, UrlEncoder encoder)
        : base(options, logger, encoder)
    {
    }

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

        var result = AuthenticateResult.Success(ticket);

        return Task.FromResult(result);
    }
}

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);
}
[TestMethod]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddAuthentication(defaultScheme: "TestScheme")
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    "TestScheme", options => { });
        });
    })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

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

    // Assert
    Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
[Test]
public async Task Get_SecurePageIsReturnedForAnAuthenticatedUser()
{
    // Arrange
    var client = _factory.WithWebHostBuilder(builder =>
    {
        builder.ConfigureTestServices(services =>
        {
            services.AddAuthentication(defaultScheme: "TestScheme")
                .AddScheme<AuthenticationSchemeOptions, TestAuthHandler>(
                    "TestScheme", options => { });
        });
    })
        .CreateClient(new WebApplicationFactoryClientOptions
        {
            AllowAutoRedirect = false,
        });

    client.DefaultRequestHeaders.Authorization =
        new AuthenticationHeaderValue(scheme: "TestScheme");

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

    // Assert
    Assert.That(response.StatusCode, Is.EqualTo(HttpStatusCode.OK));
}

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

            services.Remove(dbContextDescriptor);

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

            services.Remove(dbConnectionDescriptor);

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

                return connection;
            });

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

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

            services.Remove(dbContextDescriptor);

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

            services.Remove(dbConnectionDescriptor);

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

                return connection;
            });

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

        builder.UseEnvironment("Development");
    }
}

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.

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

När testerna av testklassen har körts TestServer och HttpClient tas bort när NUnit tar bort WebApplicationFactory i TearDown -metoden. Om objekt som instansieras av utvecklaren kräver bortskaffande ska du ta bort dem i TearDown metoden. 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 callback-funktion körs . 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