Sdílet prostřednictvím


Integrační testy v ASP.NET Core

Jos van der Til, Martin Costello a Javier Calvarro Nelson.

Integrační testy zajišťují správné fungování komponent aplikace na úrovni, která zahrnuje podpůrnou infrastrukturu aplikace, jako je databáze, systém souborů a síť. ASP.NET Core podporuje integrační testy s využitím architektury testování jednotek s testovacím webovým hostitelem a testovacím serverem v paměti.

Tento článek předpokládá základní znalost jednotkových testů. Pokud neznáte koncepty testů, přečtěte si článek Testování v .NET a jeho propojený obsah.

Zobrazení nebo stažení ukázkového kódu (postup stažení)

Ukázková aplikace je aplikace Razor Pages a předpokládá základní znalost Razor Pages. Pokud Razor Pages neznáte, přečtěte si následující články:

Pro testování SPA doporučujeme nástroj, jako je Playwright pro .NET, který dokáže automatizovat prohlížeč.

Úvod do integračních testů

Integrační testy vyhodnocují komponenty aplikace na širší úrovni než testy jednotek. Testy jednotek se používají k testování izolovaných komponent softwaru, jako jsou jednotlivé metody tříd. Testy integrace ověřují, že dvě nebo více součástí aplikace spolupracují a vytvářejí očekávaný výsledek, a to včetně všech komponent potřebných k úplnému zpracování požadavku.

Tyto širší testy se používají k otestování infrastruktury a celé architektury aplikace, často včetně následujících komponent:

  • Databáze
  • Systém souborů
  • Síťová zařízení
  • Kanál odpovědi požadavku

Testy jednotek používají fiktivní komponenty, označované jako falešné objekty nebo mock objekty, místo komponent infrastruktury.

Na rozdíl od jednotkových testů jsou testy integrační:

  • Použijte skutečné komponenty, které aplikace používá v produkčním prostředí.
  • Vyžaduje více kódu a zpracování dat.
  • Bude to trvat déle.

Proto omezte použití integračních testů na nejdůležitější scénáře infrastruktury. Pokud je možné testovat chování pomocí testu jednotek nebo integračního testu, zvolte test jednotek.

V diskusích o integračních testech se testovaný projekt často nazývá systém pod testem nebo "SUT". "SUT" se používá v tomto článku k odkazování na aplikaci ASP.NET Core, která se testuje.

Nezapisujte integrační testy pro každou permutaci přístupu k datům a souborům v databázích a souborových systémech. Bez ohledu na to, kolik míst v aplikaci interaguje s databázemi a souborovými systémy, obvykle stačí zaměřená sada testů integrace pro čtení, zápis, aktualizaci a odstranění k dostatečnému testování komponentů databáze a souborového systému. Pro rutinní testy logiky metody, které s těmito komponentami pracují, použijte testy jednotek. Při testování jednotek může použití falešných nebo napodobených infrastruktur vést k rychlejšímu provádění testů.

ASP.NET Core integrační testy

Integrační testy v ASP.NET Core vyžadují následující:

  • Projekt testů se používá k zahrnutí a provádění testů. Testovací projekt má odkaz na SUT.
  • Testovací projekt vytvoří testovacího webového hostitele pro SUT a použije klienta testovacího serveru ke zpracování požadavků a odpovědí pomocí SUT.
  • Spouštěč testů se používá k provádění testů a hlášení výsledků testu.

Integrační testy se řídí posloupností událostí, které zahrnují obvyklé testovací kroky Uspořádat, Jednat a Assert :

  1. Webový hostitel SUT je nakonfigurovaný.
  2. Vytvoří se testovací serverový klient pro odesílání požadavků do aplikace.
  3. Je spuštěn testovací krok Příprava: Testovací aplikace připraví požadavek.
  4. Testovací krok Act je spuštěn: Klient odešle požadavek a obdrží odpověď.
  5. Provede se testovací krok Assert: Skutečná odpověď se ověří jako úspěšná nebo neúspěšná podle očekávané odpovědi.
  6. Proces pokračuje, dokud nejsou všechny testy provedeny.
  7. Výsledky testu jsou hlášeny.

Obvykle je testovací webový hostitel nakonfigurovaný jinak než normální webový hostitel aplikace pro testovací běhy. Pro testy se například může použít jiná databáze nebo jiná nastavení aplikace.

Komponenty infrastruktury, jako je testovací webový hostitel a testovací server v paměti (TestServer), jsou poskytovány nebo spravovány balíčkem Microsoft.AspNetCore.Mvc.Testing . Použití tohoto balíčku zjednodušuje vytváření a spouštění testů.

Balíček Microsoft.AspNetCore.Mvc.Testing zpracovává následující úlohy:

  • Zkopíruje soubor závislostí (.deps) z SUT do adresáře testovacího bin projektu.
  • Nastaví kořen obsahu na kořen projektu SUT tak, aby při spuštění testů byly statické soubory a stránky/zobrazení dostupné.
  • Poskytuje třídu WebApplicationFactory pro zjednodušení inicializace SUT pomocí .

Dokumentace k testům jednotek popisuje, jak nastavit testovací projekt a spouštěč testů, spolu s podrobnými pokyny ke spouštění testů a doporučení pro pojmenování testů a tříd testů.

Oddělte testy jednotek od integračních testů do různých projektů. Oddělení testů:

  • Pomáhá zajistit, aby součásti testování infrastruktury nejsou omylem zahrnuty do testů jednotek.
  • Umožňuje kontrolu nad tím, jakou sadu testů se spouští.

Konfigurace pro testy Razor aplikací Pages a aplikací MVC není prakticky nijak rozdílná. Jediný rozdíl spočívá v tom, jak jsou testy pojmenovány. Razor V aplikaci Pages jsou testy koncových bodů stránky obvykle pojmenovány za třídou modelu stránky (například IndexPageTests pro testování integrace součástí pro indexovou stránku). V aplikaci MVC jsou testy obvykle uspořádány podle tříd kontroleru a pojmenovány podle kontrolerů, které testují (například HomeControllerTests pro testování integrace součástí pro Home kontroler).

Požadavky na testovací aplikaci

Testovací projekt musí:

Tyto požadavky se dají zobrazit v ukázkové aplikaci. tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj Zkontrolujte soubor. Ukázková aplikace používá testovací architekturu xUnit a knihovnu analyzátoru AngleSharp , takže ukázková aplikace také odkazuje:

V aplikacích, které používají xunit.runner.visualstudio verzi 2.4.2 nebo novější, musí testovací projekt odkazovat na Microsoft.NET.Test.Sdk balíček.

Entity Framework Core se také používá v testech. Prohlédněte si soubor projektu na GitHubu.

Prostředí SUT

Pokud prostředí SUT není nastavené, prostředí se ve výchozím nastavení nastaví na Vývoj.

Základní testy s výchozím WebApplicationFactory

WebApplicationFactory<TEntryPoint> slouží k vytvoření TestServer pro integrační testy. TEntryPoint je vstupní bod třídy SUT, obvykle Program.cs.

Třídy testů implementují rozhraní třídní fixatury () aby se označilo, že třída obsahuje testy, a poskytují sdílené instance objektů pro testy ve třídě.

Následující testovací třída BasicTests používá WebApplicationFactory ke spuštění SUT a poskytuje HttpClient testovací metodě Get_EndpointsReturnSuccessAndCorrectContentType. Metoda ověří, že stavový kód odpovědi je úspěšný (200–299) a hlavička Content-Type je text/html; charset=utf-8 pro několik stránek aplikace.

CreateClient() vytvoří instanci HttpClient , která automaticky sleduje přesměrování a zpracovává soubory cookie.

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

Ve výchozím nastavení se soubory cookie, které nejsou nezbytné, nezachovávají mezi požadavky, když jsou povoleny zásady souhlasu podle Obecného nařízení o ochraně osobních údajů (GDPR). Pokud chcete zachovat soubory cookie, které nejsou nezbytné, například soubory používané poskytovatelem TempData, označte je v testech jako nezbytné. Pokyny k označení cookie jako nezbytné naleznete v části Základní soubory cookie.

AngleSharp vs Application Parts pro antiforgery ověření

Tento článek používá analyzátor AngleSharp ke zpracování kontrol proti padělání tím, že načítá stránky a analyzuje HTML. Pokud chcete otestovat koncové body zobrazení kontroleru a Razor stránek na nižší úrovni, aniž byste se museli zabývat tím, jak se vykreslují v prohlížeči, zvažte použití Application Parts. Přístup Application Parts vloží kontroler nebo Razor stránku do aplikace, což lze použít k odesílání JSON požadavků pro získání potřebných hodnot. Další informace najdete v blogu o testování integrace prostředků ASP.NET Core chráněných antiforgery pomocí částí aplikace a přidruženém úložišti GitHub, od Martina Costella.

Přizpůsobení služby WebApplicationFactory

Konfiguraci webového hostitele lze vytvořit nezávisle na testovacích třídách tím, že zdědíte z WebApplicationFactory<TEntryPoint>, abyste vytvořili jednu nebo více vlastních továren:

  1. Dědit z WebApplicationFactory a přepsat ConfigureWebHost. Tento prvek IWebHostBuilder umožňuje konfiguraci kolekce služeb pomocí 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");
        }
    }
    

    Naplnění databáze v ukázkové aplikaci se provádí metodou InitializeDbForTests. Metoda je popsána v sekci Ukázka integračních testů: Organizace testovací aplikace.

    Kontext databáze SUT je registrován v Program.cs. Zpětné volání testovací aplikace builder.ConfigureServices se spustí po spuštění kódu aplikace Program.cs . Chcete-li použít jinou databázi pro testy, než je databáze aplikace, musí být kontext databáze aplikace nahrazen v builder.ConfigureServices.

    Ukázková aplikace najde popisovač služby pro kontext databáze a pomocí popisovače odebere registraci služby. Továrna pak přidá nový ApplicationDbContext, který využívá databázi v paměti pro testování.

    Pokud se chcete připojit k jiné databázi, změňte soubor DbConnection. Použití testovací databáze SQL Serveru:

  1. Použijte vlastní CustomWebApplicationFactory v testovacích třídách. Příklad níže používá továrnu ve třídě 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();
        }
    

    Klient ukázkové aplikace je nakonfigurovaný tak, aby zabránil HttpClient následujícím přesměrováním. Jak je vysvětleno později v části Napodobení ověřování , umožňuje testům zkontrolovat výsledek první odpovědi aplikace. První odpověď v mnoha těchto testech je přesměrování s hlavičkou Location.

  2. Typický test používá HttpClient metody a pomocné metody ke zpracování požadavku a odpovědi:

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

Všechny požadavky POST na SUT musí splňovat antiforgery kontrolu, která je automaticky provedena systémem ochrany dat aplikace. Aby bylo možné zajistit požadavek POST testu, musí testovací aplikace:

  1. Vytvořte žádost o stránku.
  2. Parsujte antiforgery token cookie a token pro ověření požadavku z odpovědi.
  3. Vytvořte žádost POST s použitím antiforgery cookie a ověřovacího tokenu žádosti.

Pomocné SendAsync rozšiřující metody (Helpers/HttpClientExtensions.cs) a GetDocumentAsync pomocná metoda (Helpers/HtmlHelpers.cs) v ukázkové aplikaci používají analyzátor AngleSharp ke zpracování kontroly antiforgery následujícími metodami:

  • GetDocumentAsync: Přijme HttpResponseMessage a vrátí hodnotu IHtmlDocument. GetDocumentAsync používá továrnu, která připraví virtuální odpověď na základě původní HttpResponseMessage. Další informace najdete v dokumentaci AngleSharp.
  • SendAsync metody rozšíření pro HttpClient vytvoření žádosti HttpRequestMessage a volání SendAsync(HttpRequestMessage) k odeslání žádostí do SUT. Přetížení pro SendAsync přijímají HTML formulář (IHtmlFormElement) a následující:
    • Tlačítko Odeslat formulář (IHtmlElement)
    • Kolekce hodnot formulářů (IEnumerable<KeyValuePair<string, string>>)
    • Tlačítko Odeslat (IHtmlElement) a hodnoty formuláře (IEnumerable<KeyValuePair<string, string>>)

AngleSharp je knihovna analýzy třetí strany používaná pro demonstrační účely v tomto článku a ukázkové aplikaci. AngleSharp se nepodporuje ani nevyžaduje pro testování integrace aplikací ASP.NET Core. Dalšími analyzátory, které lze použít, je například Html Agility Pack (HAP). Dalším přístupem je napsat kód pro zpracování ověřovacího tokenu, který systém antiforgery používá pro ověřování žádostí, a řídit antiforgery cookie přímo. Další informace najdete v části AngleSharp vs Application Parts kontroly antiforgery v tomto článku.

Poskytovatel databáze EF-Core v paměti lze použít pro omezené a základní testování, avšak pro testování v paměti se doporučuje poskytovatel SQLite.

Viz Rozšíření spuštění pomocí startovacích filtrů, který ukazuje, jak nakonfigurovat middleware pomocí IStartupFilter, což je užitečné, když test vyžaduje vlastní službu nebo middleware.

Přizpůsobení klienta pomocí withWebHostBuilder

Pokud je v rámci testovací metody vyžadována další konfigurace, WithWebHostBuilder vytvoří novou WebApplicationFactory s IWebHostBuilder, která je dále přizpůsobena pomocí konfigurace.

Ukázkový kód volá WithWebHostBuilder, aby nahradil nakonfigurované služby testovacími zástupci. Další informace a příklady použití najdete v části Vložení napodobených služeb v tomto článku.

Metoda Post_DeleteMessageHandler_ReturnsRedirectToRoot testu ukázkové aplikace ukazuje použití WithWebHostBuilder. Tento test provede odstranění záznamu v databázi aktivací odeslání formuláře v SUT.

Protože jiný test ve IndexPageTests třídě provádí operaci, která odstraní všechny záznamy v databázi a může běžet před Post_DeleteMessageHandler_ReturnsRedirectToRoot metodou, databáze bude znovu provedena v této testovací metodě, aby se zajistilo, že záznam existuje pro SUT k odstranění. Výběr prvního tlačítka messages pro odstranění formuláře v SUT se simuluje v požadavku na 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("/"));
}

Možnosti klienta

Podívejte se na WebApplicationFactoryClientOptions stránku s výchozími a dostupnými možnostmi při vytváření HttpClient instancí.

WebApplicationFactoryClientOptions Vytvořte třídu a předejte ji metodě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();
    }

POZNÁMKA: Pokud se chcete vyhnout upozorněním přesměrování HTTPS v protokolech při použití middlewaru přesměrování HTTPS, nastavte BaseAddress = new Uri("https://localhost")

Vložte simulované služby

Služby je možné přepsat v testu voláním ConfigureTestServices na builderu hostitele. K určení rozsahu přepsaných služeb na samotný test se používá metoda WithWebHostBuilder k načtení host builderu. To lze vidět v následujících testech:

Příklad SUT obsahuje vymezenou službu, která vrací citát. Citace je vložena do skrytého pole na stránce Index, když je stránka Index žádána.

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

Při spuštění aplikace SUT se vygeneruje následující kód:

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

K otestování injektáže služby a nabídky v integračním testu se do testu vloží napodobená služba. Služba napodobení nahradí aplikaci QuoteService službou poskytovanou testovací aplikací, která se nazývá 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 je volána a služba s vymezeným oborem je zaregistrovaná:

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

Kód vytvořený během provádění testu odráží uvozovkový text zadaný TestQuoteService, a proto kontrola projde:

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

Simulované ověřování

Testy ve AuthTests třídě kontrolují, že zabezpečený koncový bod:

  • Přesměruje neověřeného uživatele na přihlašovací stránku aplikace.
  • Vrátí obsah pro ověřeného uživatele.

V SUT stránka /SecurePage používá AuthorizePage konvenci pro aplikaci AuthorizeFilter na stránku. Další informace najdete v tématu Razor Konvence autorizace stránek.

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

V testu Get_SecurePageRedirectsAnUnauthenticatedUser je možnost WebApplicationFactoryClientOptions nastavena tak, aby zakázala přesměrování nastavením AllowAutoRedirect na 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"));
}

Když zakážete klientovi sledovat přesměrování, můžete provést následující kontroly:

  • Stavový kód vrácený SUT lze zkontrolovat podle očekávaného HttpStatusCode.Redirect výsledku, nikoli konečný stavový kód po přesměrování na přihlašovací stránku, což by bylo HttpStatusCode.OK.
  • Hodnota záhlaví Location v hlavičkách odpovědi se kontroluje, aby se ověřilo, že začíná na http://localhost/Identity/Account/Login, nikoli na konečné odpovědi přihlašovací stránky, kde by záhlaví Location nebylo k dispozici.

Testovací aplikace může simulovat AuthenticationHandler<TOptions> v ConfigureTestServices za účelem testování aspektů ověřování a autorizace. Minimální scénář vrátí 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);
    }
}

Když je schéma ověřování nastaveno na TestAuthHandler, volá se TestScheme k ověření uživatele, kde AddAuthentication je registrován pro ConfigureTestServices. Je důležité, TestScheme aby schéma odpovídalo schématu, které vaše aplikace očekává. Jinak ověřování nebude fungovat.

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

Další informace o WebApplicationFactoryClientOptionsnaleznete v části možnosti klienta.

Základní testy pro ověřovací middleware

Základní testy middlewaru ověřování najdete v tomto úložišti GitHubu. Obsahuje testovací server specifický pro testovací scénář.

Nastavení prostředí

Nastavte prostředí ve vlastní objektu pro vytváření aplikací:

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

Způsob, jakým testovací infrastruktura odvodí kořenovou cestu obsahu aplikace

Konstruktor WebApplicationFactory odvodí kořenovou cestu obsahu aplikace tak, že vyhledá WebApplicationFactoryContentRootAttribute v sestavení obsahující integrační testy s klíčem, který se rovná TEntryPoint sestavení System.Reflection.Assembly.FullName. Pokud se atribut se správným klíčem nenajde, WebApplicationFactory vrátí se zpět do hledání souboru řešení (.sln) a připojí TEntryPoint název sestavení do adresáře řešení. Kořenový adresář aplikace (kořenová cesta k obsahu) slouží ke zjišťování zobrazení a souborů obsahu.

Zakázat stínové kopírování

Stínová kopie způsobí, že testy se spustí v jiném adresáři než ve výstupním adresáři. Pokud vaše testy spoléhají na načítání souborů vzhledem k Assembly.Location a dochází k problémům, možná budete muset zakázat stínové kopírování.

Pokud chcete zakázat stínové kopírování při použití xUnit, vytvořte v adresáři testovacího xunit.runner.json projektu soubor se správným nastavením konfigurace:

{
  "shadowCopy": false
}

Likvidace objektů

Po provedení testů implementace IClassFixture jsou TestServer a HttpClient uvolněny, když xUnit uvolní WebApplicationFactory. Pokud instance objektů vytvořených vývojářem vyžadují odstranění, odstraňte je v implementaci IClassFixture. Další informace naleznete v tématu Implementace metody Dispose.

Po provedení testů TestClass jsou TestServer a HttpClient odstraněny, když MSTest odstraní WebApplicationFactory v metodě ClassCleanup. Pokud instance objektů vytvořených vývojářem vyžaduje odstranění, odstraňte je v ClassCleanup metodě. Další informace naleznete v tématu Implementace metody Dispose.

Po provedení testů třídy testu jsou TestServer a HttpClient uvolněny, když NUnit odstraní WebApplicationFactory v metodě TearDown. Pokud instance objektů vytvořených vývojářem vyžaduje odstranění, odstraňte je v TearDown metodě. Další informace naleznete v tématu Implementace metody Dispose.

Ukázka integračních testů

Ukázková aplikace se skládá ze dvou aplikací:

Aplikace Adresář projektu Popis
Aplikace zpráv (SUT) src/RazorPagesProject Umožňuje uživateli přidávat, odstraňovat, odstraňovat všechny a analyzovat zprávy.
Testovací aplikace tests/RazorPagesProject.Tests Používá se k provádění integračního testování SUT.

Testy je možné spouštět pomocí integrovaných testovacích funkcí IDE, například Visual Studio. Pokud používáte Visual Studio Code nebo příkazový řádek, spusťte v adresáři tests/RazorPagesProject.Tests následující příkaz:

dotnet test

Organizace aplikace zpráv (SUT)

SUT je Razor systém zpráv Pages s následujícími vlastnostmi:

  • Indexová stránka aplikace (Pages/Index.cshtml a Pages/Index.cshtml.cs) poskytuje uživatelské rozhraní a metody modelu stránky pro řízení sčítání, odstranění a analýzy zpráv (průměrná slova na zprávu).
  • Zpráva je popsána Message třídou (Data/Message.cs) se dvěma vlastnostmi: Id (klíč) a Text (zpráva). Vlastnost Text je povinná a omezená na 200 znaků.
  • Zprávy se ukládají pomocí databáze v paměti entity Framework†.
  • Aplikace obsahuje vrstvu přístupu k datům (DAL) ve své třídě AppDbContext kontextu databáze (Data/AppDbContext.cs).
  • Pokud je databáze při spuštění aplikace prázdná, úložiště zpráv se inicializuje se třemi zprávami.
  • Aplikace zahrnuje /SecurePage, ke kterému má přístup pouze ověřený uživatel.

Článek EF, Test s InMemory vysvětluje, jak používat in-memory databázi pro testování s MSTest. Toto téma používá testovací architekturu xUnit . Koncepty testů a implementace testů v různých testovacích architekturách jsou podobné, ale ne identické.

I když aplikace nepoužívá vzor úložiště a není efektivním příkladem Unit of Work (UoW) pattern, stránky podporují tyto vzory vývoje. Další informace najdete v tématu Návrh vrstvy trvalosti infrastruktury a logiky kontroleru testů (ukázka implementuje vzor úložiště).

Testovací organizace aplikací

Testovací aplikace je konzolová aplikace uvnitř tests/RazorPagesProject.Tests adresáře.

Testovací adresář aplikace Popis
AuthTests Obsahuje testovací metody pro:
  • Přístup k zabezpečené stránce neověřeným uživatelem
  • Přístup ověřeného uživatele k zabezpečené stránce s využitím simulace AuthenticationHandler<TOptions>.
  • Získání profilu uživatele GitHubu a kontrola přihlášení uživatele profilu
BasicTests Obsahuje testovací metodu pro směrování a typ obsahu.
IntegrationTests Obsahuje integrační testy stránky indexu pomocí vlastní WebApplicationFactory třídy.
Helpers/Utilities
  • Utilities.cs obsahuje metodu použitou InitializeDbForTests k vytvoření databáze s testovacími daty.
  • HtmlHelpers.cs poskytuje metodu, která vrací AngleSharp IHtmlDocument pro použití testovacími metodami.
  • HttpClientExtensions.cs poskytuje přetížení pro SendAsync k odesílání požadavků do SUT.

Testovací architektura je xUnit. Integrační testy se provádějí pomocí Microsoft.AspNetCore.TestHost, která zahrnuje TestServer. Vzhledem k tomu, že se Microsoft.AspNetCore.Mvc.Testing balíček používá ke konfiguraci testovacího hostitele a testovacího serveru, TestHost nevyžadují a TestServer balíčky přímé odkazy na balíčky v souboru projektu testovací aplikace nebo konfiguraci vývojáře v testovací aplikaci.

Integrační testy obvykle před spuštěním testu vyžadují malou datovou sadu v databázi. Například test mazání vyžaduje odstranění záznamu z databáze, takže je třeba, aby databáze měla alespoň jeden záznam, aby žádost o odstranění byla úspěšná.

Ukázková aplikace naplní databázi třemi zprávami v Utilities.cs, které testy mohou použít při spuštění:

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

Kontext databáze SUT je registrován v Program.cs. Zpětné volání testovací aplikace builder.ConfigureServices se spustí po spuštění kódu aplikace Program.cs . Chcete-li pro testy použít jinou databázi, musí být kontext databáze aplikace nahrazen v builder.ConfigureServices. Další informace najdete v části Přizpůsobení WebApplicationFactory.

Další materiály

V tomto tématu se předpokládá základní znalost jednotkových testů. Pokud neznáte koncepty testů, přečtěte si téma Testování částí v tématech .NET Core a .NET Standard a jeho propojený obsah.

Zobrazení nebo stažení ukázkového kódu (postup stažení)

Ukázková aplikace je aplikace Razor Pages a předpokládá základní znalost Razor Pages. Neznáte-li Razor stránky, projděte si následující témata:

Poznámka:

Pro testování SPA doporučujeme nástroj, jako Playwright pro .NET, který může automatizovat prohlížeč.

Úvod do integračních testů

Integrační testy vyhodnocují komponenty aplikace na širší úrovni než testy jednotek. Testy jednotek se používají k testování izolovaných komponent softwaru, jako jsou jednotlivé metody tříd. Testy integrace ověřují, že dvě nebo více součástí aplikace spolupracují a vytvářejí očekávaný výsledek, a to včetně všech komponent potřebných k úplnému zpracování požadavku.

Tyto širší testy se používají k otestování infrastruktury a celé architektury aplikace, často včetně následujících komponent:

  • Databáze
  • Systém souborů
  • Síťová zařízení
  • Kanál odpovědi požadavku

Testy jednotek používají fiktivní komponenty, označované jako falešné objekty nebo mock objekty, místo komponent infrastruktury.

Na rozdíl od jednotkových testů jsou testy integrační:

  • Použijte skutečné komponenty, které aplikace používá v produkčním prostředí.
  • Vyžaduje více kódu a zpracování dat.
  • Bude to trvat déle.

Proto omezte použití integračních testů na nejdůležitější scénáře infrastruktury. Pokud je možné testovat chování pomocí testu jednotek nebo integračního testu, zvolte test jednotek.

V diskusích o integračních testech se testovaný projekt často nazývá systém pod testem nebo "SUT". "SUT" se používá v tomto článku k odkazování na aplikaci ASP.NET Core, která se testuje.

Nezapisujte integrační testy pro každou permutaci přístupu k datům a souborům v databázích a souborových systémech. Bez ohledu na to, kolik míst v aplikaci interaguje s databázemi a souborovými systémy, obvykle stačí zaměřená sada testů integrace pro čtení, zápis, aktualizaci a odstranění k dostatečnému testování komponentů databáze a souborového systému. Pro rutinní testy logiky metody, které s těmito komponentami pracují, použijte testy jednotek. Při testování jednotek může použití falešných nebo napodobených infrastruktur vést k rychlejšímu provádění testů.

ASP.NET Core integrační testy

Integrační testy v ASP.NET Core vyžadují následující:

  • Projekt testů se používá k zahrnutí a provádění testů. Testovací projekt má odkaz na SUT.
  • Testovací projekt vytvoří testovacího webového hostitele pro SUT a použije klienta testovacího serveru ke zpracování požadavků a odpovědí pomocí SUT.
  • Spouštěč testů se používá k provádění testů a hlášení výsledků testu.

Integrační testy se řídí posloupností událostí, které zahrnují obvyklé testovací kroky Uspořádat, Jednat a Assert :

  1. Webový hostitel SUT je nakonfigurovaný.
  2. Vytvoří se testovací serverový klient pro odesílání požadavků do aplikace.
  3. Je spuštěn testovací krok Příprava: Testovací aplikace připraví požadavek.
  4. Testovací krok Act je spuštěn: Klient odešle požadavek a obdrží odpověď.
  5. Provede se testovací krok Assert: Skutečná odpověď se ověří jako úspěšná nebo neúspěšná podle očekávané odpovědi.
  6. Proces pokračuje, dokud nejsou všechny testy provedeny.
  7. Výsledky testu jsou hlášeny.

Obvykle je testovací webový hostitel nakonfigurovaný jinak než normální webový hostitel aplikace pro testovací běhy. Pro testy se například může použít jiná databáze nebo jiná nastavení aplikace.

Komponenty infrastruktury, jako je testovací webový hostitel a testovací server v paměti (TestServer), jsou poskytovány nebo spravovány balíčkem Microsoft.AspNetCore.Mvc.Testing . Použití tohoto balíčku zjednodušuje vytváření a spouštění testů.

Balíček Microsoft.AspNetCore.Mvc.Testing zpracovává následující úlohy:

  • Zkopíruje soubor závislostí (.deps) z SUT do adresáře testovacího bin projektu.
  • Nastaví kořen obsahu na kořen projektu SUT tak, aby při spuštění testů byly statické soubory a stránky/zobrazení dostupné.
  • Poskytuje třídu WebApplicationFactory pro zjednodušení inicializace SUT pomocí .

Dokumentace k testům jednotek popisuje, jak nastavit testovací projekt a spouštěč testů, spolu s podrobnými pokyny ke spouštění testů a doporučení pro pojmenování testů a tříd testů.

Oddělte testy jednotek od integračních testů do různých projektů. Oddělení testů:

  • Pomáhá zajistit, aby součásti testování infrastruktury nejsou omylem zahrnuty do testů jednotek.
  • Umožňuje kontrolu nad tím, jakou sadu testů se spouští.

Konfigurace pro testy Razor aplikací Pages a aplikací MVC není prakticky nijak rozdílná. Jediný rozdíl spočívá v tom, jak jsou testy pojmenovány. Razor V aplikaci Pages jsou testy koncových bodů stránky obvykle pojmenovány za třídou modelu stránky (například IndexPageTests pro testování integrace součástí pro indexovou stránku). V aplikaci MVC jsou testy obvykle uspořádány podle tříd kontroleru a pojmenovány podle kontrolerů, které testují (například HomeControllerTests pro testování integrace součástí pro Home kontroler).

Požadavky na testovací aplikaci

Testovací projekt musí:

Tyto požadavky se dají zobrazit v ukázkové aplikaci. tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj Zkontrolujte soubor. Ukázková aplikace používá testovací architekturu xUnit a knihovnu analyzátoru AngleSharp , takže ukázková aplikace také odkazuje:

V aplikacích, které používají xunit.runner.visualstudio verzi 2.4.2 nebo novější, musí testovací projekt odkazovat na Microsoft.NET.Test.Sdk balíček.

Entity Framework Core se také používá v testech. Odkazy na aplikaci:

Prostředí SUT

Pokud prostředí SUT není nastavené, prostředí se ve výchozím nastavení nastaví na Vývoj.

Základní testy s výchozím WebApplicationFactory

WebApplicationFactory<TEntryPoint> slouží k vytvoření TestServer pro integrační testy. TEntryPoint je třídou vstupního bodu SUT, obvykle třída Startup.

Třídy testů implementují rozhraní třídní fixatury () aby se označilo, že třída obsahuje testy, a poskytují sdílené instance objektů pro testy ve třídě.

Následující testovací třída BasicTests používá WebApplicationFactory ke spuštění SUT a poskytuje HttpClient testovací metodě Get_EndpointsReturnSuccessAndCorrectContentType. Metoda zkontroluje, jestli je stavový kód odpovědi úspěšný (stavové kódy v rozsahu 200–299) a zda je Content-Type záhlaví text/html; charset=utf-8 na několika stránkách aplikace.

CreateClient() vytvoří instanci HttpClient , která automaticky sleduje přesměrování a zpracovává soubory cookie.

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

Ve výchozím nastavení se při povolení zásad souhlasu GDPR nezachovají soubory cookie, které nejsou nezbytné. Pokud chcete zachovat soubory cookie, které nejsou nezbytné, například soubory používané poskytovatelem TempData, označte je v testech jako nezbytné. Pokyny k označení cookie jako nezbytné naleznete v části Základní soubory cookie.

Přizpůsobení služby WebApplicationFactory

Konfiguraci webového hostitele lze vytvořit nezávisle na testovacích třídách tím, že zdědíte z WebApplicationFactory, abyste vytvořili jednu nebo více vlastních továren:

  1. Dědit z WebApplicationFactory a přepsat ConfigureWebHost. IWebHostBuilder umožňuje konfiguraci kolekce služeb pomocí 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);
                    }
                }
            });
        }
    }
    

    Naplnění databáze v ukázkové aplikaci se provádí metodou InitializeDbForTests. Metoda je popsána v sekci Ukázka integračních testů: Organizace testovací aplikace.

    Kontext databáze SUT je registrován v jeho Startup.ConfigureServices metodě. Zpětné volání testovací aplikace builder.ConfigureServices se spustí po spuštění kódu aplikace Startup.ConfigureServices . Pořadí provádění je zásadní změnou obecného hostitele s vydáním ASP.NET Core 3.0. Chcete-li použít jinou databázi pro testy, než je databáze aplikace, musí být kontext databáze aplikace nahrazen v builder.ConfigureServices.

    U jednotek SUT, které stále používají webového hostitele, se zpětné volání testovací aplikace builder.ConfigureServices provede před kódem Startup.ConfigureServices SUT. Zpětná volání testovací aplikace builder.ConfigureTestServices se spustí po.

    Ukázková aplikace najde popisovač služby pro kontext databáze a pomocí popisovače odebere registraci služby. Dále továrna přidá nové ApplicationDbContext, které používá databázi v paměti pro testy.

    Pokud se chcete připojit k jiné databázi než databáze v paměti, změňte UseInMemoryDatabase volání pro připojení kontextu k jiné databázi. Použití testovací databáze SQL Serveru:

    services.AddDbContext<ApplicationDbContext>((options, context) => 
    {
        context.UseSqlServer(
            Configuration.GetConnectionString("TestingDbConnectionString"));
    });
    
  2. Použijte vlastní CustomWebApplicationFactory v testovacích třídách. Příklad níže používá továrnu ve třídě 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
                });
        }
    

    Klient ukázkové aplikace je nakonfigurovaný tak, aby zabránil HttpClient následujícím přesměrováním. Jak je vysvětleno později v části Napodobení ověřování , umožňuje testům zkontrolovat výsledek první odpovědi aplikace. První odpověď v mnoha těchto testech je přesměrování s hlavičkou Location.

  3. Typický test používá HttpClient metody a pomocné metody ke zpracování požadavku a odpovědi:

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

Všechny požadavky POST na SUT musí splňovat antiforgery kontrolu, která je automaticky provedena systémem ochrany dat aplikace. Aby bylo možné zajistit požadavek POST testu, musí testovací aplikace:

  1. Vytvořte žádost o stránku.
  2. Parsujte antiforgery token cookie a token pro ověření požadavku z odpovědi.
  3. Vytvořte žádost POST s použitím antiforgery cookie a ověřovacího tokenu žádosti.

Pomocné SendAsync rozšiřující metody (Helpers/HttpClientExtensions.cs) a GetDocumentAsync pomocná metoda (Helpers/HtmlHelpers.cs) v ukázkové aplikaci používají analyzátor AngleSharp ke zpracování kontroly antiforgery následujícími metodami:

  • GetDocumentAsync: Přijme HttpResponseMessage a vrátí hodnotu IHtmlDocument. GetDocumentAsync používá továrnu, která připraví virtuální odpověď na základě původní HttpResponseMessage. Další informace najdete v dokumentaci AngleSharp.
  • SendAsync metody rozšíření pro HttpClient vytvoření žádosti HttpRequestMessage a volání SendAsync(HttpRequestMessage) k odeslání žádostí do SUT. Přetížení pro SendAsync přijímají HTML formulář (IHtmlFormElement) a následující:
    • Tlačítko Odeslat formulář (IHtmlElement)
    • Kolekce hodnot formulářů (IEnumerable<KeyValuePair<string, string>>)
    • Tlačítko Odeslat (IHtmlElement) a hodnoty formuláře (IEnumerable<KeyValuePair<string, string>>)

Poznámka:

AngleSharp je knihovna analýzy třetí strany používaná pro demonstrační účely v tomto tématu a ukázkovou aplikaci. AngleSharp se nepodporuje ani nevyžaduje pro testování integrace aplikací ASP.NET Core. Dalšími analyzátory, které lze použít, je například Html Agility Pack (HAP). Dalším přístupem je napsat kód pro zpracování ověřovacího tokenu, který systém antiforgery používá pro ověřování žádostí, a řídit antiforgery cookie přímo.

Poznámka:

Poskytovatel databáze EF-Core v paměti lze použít pro omezené a základní testování, avšak pro testování v paměti se doporučuje zprostředkovatel SQLite.

Přizpůsobení klienta pomocí withWebHostBuilder

Pokud je v rámci testovací metody vyžadována další konfigurace, WithWebHostBuilder vytvoří novou WebApplicationFactory s IWebHostBuilder, která je dále přizpůsobena pomocí konfigurace.

Metoda Post_DeleteMessageHandler_ReturnsRedirectToRoot testu ukázkové aplikace ukazuje použití WithWebHostBuilder. Tento test provede odstranění záznamu v databázi aktivací odeslání formuláře v SUT.

Protože jiný test ve IndexPageTests třídě provádí operaci, která odstraní všechny záznamy v databázi a může běžet před Post_DeleteMessageHandler_ReturnsRedirectToRoot metodou, databáze bude znovu provedena v této testovací metodě, aby se zajistilo, že záznam existuje pro SUT k odstranění. Výběr prvního tlačítka messages pro odstranění formuláře v SUT se simuluje v požadavku na 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);
}

Možnosti klienta

Následující tabulka ukazuje výchozí WebApplicationFactoryClientOptions při vytváření HttpClient instancí.

Možnost Popis Výchozí
AllowAutoRedirect Získá nebo nastaví, zda by instance HttpClient měly automaticky sledovat odpovědi na přesměrování. true
BaseAddress Získá nebo nastaví základní adresu HttpClient instancí. http://localhost
HandleCookies Získá nebo nastaví, zda HttpClient instance mají zpracovávat soubory cookie. true
MaxAutomaticRedirections Získá nebo nastaví maximální počet odpovědí na přesměrování, které by instance HttpClient měly sledovat. 7

WebApplicationFactoryClientOptions Vytvořte třídu a předejte ji CreateClient() metodě (výchozí hodnoty se zobrazují v příkladu kódu):

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

Vložte simulované služby

Služby je možné přepsat v testu voláním ConfigureTestServices na builderu hostitele. K vložení napodobených služeb musí SUT mít Startup třídu s metodou Startup.ConfigureServices .

Příklad SUT obsahuje vymezenou službu, která vrací citát. Citace je vložena do skrytého pole na stránce Index, když je stránka Index žádána.

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

Při spuštění aplikace SUT se vygeneruje následující kód:

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

K otestování injektáže služby a nabídky v integračním testu se do testu vloží napodobená služba. Služba napodobení nahradí aplikaci QuoteService službou poskytovanou testovací aplikací, která se nazývá 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 je volána a služba s vymezeným oborem je zaregistrovaná:

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

Kód vytvořený během provádění testu odráží uvozovkový text zadaný TestQuoteService, a proto kontrola projde:

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

Simulované ověřování

Testy ve AuthTests třídě kontrolují, že zabezpečený koncový bod:

  • Přesměruje neověřeného uživatele na přihlašovací stránku aplikace.
  • Vrátí obsah pro ověřeného uživatele.

V SUT stránka /SecurePage používá AuthorizePage konvenci pro aplikaci AuthorizeFilter na stránku. Další informace najdete v tématu Razor Konvence autorizace stránek.

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

V testu Get_SecurePageRedirectsAnUnauthenticatedUser je možnost WebApplicationFactoryClientOptions nastavena tak, aby zakázala přesměrování nastavením AllowAutoRedirect na 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);
}

Když zakážete klientovi sledovat přesměrování, můžete provést následující kontroly:

  • Stavový kód vrácený SUT lze zkontrolovat podle očekávaného HttpStatusCode.Redirect výsledku, nikoli konečný stavový kód po přesměrování na přihlašovací stránku, což by bylo HttpStatusCode.OK.
  • Hodnota záhlaví Location v hlavičce odpovědi je zjištěna, aby se ověřilo, že začíná na http://localhost/Identity/Account/Login, a ne poslední odpověď z přihlašovací stránky, kde záhlaví Location nebude k dispozici.

Testovací aplikace může simulovat AuthenticationHandler<TOptions> v ConfigureTestServices za účelem testování aspektů ověřování a autorizace. Minimální scénář vrátí 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);
    }
}

Když je schéma ověřování nastaveno na TestAuthHandler, volá se Test k ověření uživatele, kde AddAuthentication je registrován pro ConfigureTestServices. Je důležité, Test aby schéma odpovídalo schématu, které vaše aplikace očekává. Jinak ověřování nebude fungovat.

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

Další informace o WebApplicationFactoryClientOptionsnaleznete v části možnosti klienta.

Nastavení prostředí

Ve výchozím nastavení je hostitel a prostředí aplikace SUT nakonfigurované tak, aby používalo vývojové prostředí. Chcete-li přepsat prostředí SUT při použití IHostBuilder:

  • Nastavte proměnnou ASPNETCORE_ENVIRONMENT prostředí (například Staging, Productionnebo jinou vlastní hodnotu, například Testing).
  • Přepište CreateHostBuilder v testovací aplikaci pro čtení proměnných prostředí s předponou ASPNETCORE.
protected override IHostBuilder CreateHostBuilder() =>
    base.CreateHostBuilder()
        .ConfigureHostConfiguration(
            config => config.AddEnvironmentVariables("ASPNETCORE"));

Pokud SUT používá webového hostitele (IWebHostBuilder), přepište CreateWebHostBuilder:

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

Způsob, jakým testovací infrastruktura odvodí kořenovou cestu obsahu aplikace

Konstruktor WebApplicationFactory odvodí kořenovou cestu obsahu aplikace tak, že vyhledá WebApplicationFactoryContentRootAttribute v sestavení obsahující integrační testy s klíčem, který se rovná TEntryPoint sestavení System.Reflection.Assembly.FullName. Pokud se atribut se správným klíčem nenajde, WebApplicationFactory vrátí se zpět do hledání souboru řešení (.sln) a připojí TEntryPoint název sestavení do adresáře řešení. Kořenový adresář aplikace (kořenová cesta k obsahu) slouží ke zjišťování zobrazení a souborů obsahu.

Zakázat stínové kopírování

Stínová kopie způsobí, že testy se spustí v jiném adresáři než ve výstupním adresáři. Pokud vaše testy spoléhají na načítání souborů vzhledem k Assembly.Location a dochází k problémům, možná budete muset zakázat stínové kopírování.

Pokud chcete zakázat stínové kopírování při použití xUnit, vytvořte v adresáři testovacího xunit.runner.json projektu soubor se správným nastavením konfigurace:

{
  "shadowCopy": false
}

Likvidace objektů

Po provedení testů implementace IClassFixture jsou TestServer a HttpClient uvolněny, když xUnit uvolní WebApplicationFactory. Pokud instance objektů vytvořených vývojářem vyžadují odstranění, odstraňte je v implementaci IClassFixture. Další informace naleznete v tématu Implementace metody Dispose.

Ukázka integračních testů

Ukázková aplikace se skládá ze dvou aplikací:

Aplikace Adresář projektu Popis
Aplikace zpráv (SUT) src/RazorPagesProject Umožňuje uživateli přidávat, odstraňovat, odstraňovat všechny a analyzovat zprávy.
Testovací aplikace tests/RazorPagesProject.Tests Používá se k provádění integračního testování SUT.

Testy je možné spouštět pomocí integrovaných testovacích funkcí IDE, například Visual Studio. Pokud používáte Visual Studio Code nebo příkazový řádek, spusťte v adresáři tests/RazorPagesProject.Tests následující příkaz:

dotnet test

Organizace aplikace zpráv (SUT)

SUT je Razor systém zpráv Pages s následujícími vlastnostmi:

  • Indexová stránka aplikace (Pages/Index.cshtml a Pages/Index.cshtml.cs) poskytuje uživatelské rozhraní a metody modelu stránky pro řízení sčítání, odstranění a analýzy zpráv (průměrná slova na zprávu).
  • Zpráva je popsána Message třídou (Data/Message.cs) se dvěma vlastnostmi: Id (klíč) a Text (zpráva). Vlastnost Text je povinná a omezená na 200 znaků.
  • Zprávy se ukládají pomocí databáze v paměti entity Framework†.
  • Aplikace obsahuje vrstvu přístupu k datům (DAL) ve své třídě AppDbContext kontextu databáze (Data/AppDbContext.cs).
  • Pokud je databáze při spuštění aplikace prázdná, úložiště zpráv se inicializuje se třemi zprávami.
  • Aplikace zahrnuje /SecurePage, ke kterému má přístup pouze ověřený uživatel.

Téma EF, Test s InMemory, vysvětluje, jak používat databázi v paměti pro testy s MSTestem. Toto téma používá testovací architekturu xUnit . Koncepty testů a implementace testů v různých testovacích architekturách jsou podobné, ale ne identické.

I když aplikace nepoužívá vzor úložiště a není efektivním příkladem Unit of Work (UoW) pattern, stránky podporují tyto vzory vývoje. Další informace najdete v tématu Návrh vrstvy trvalosti infrastruktury a logiky kontroleru testů (ukázka implementuje vzor úložiště).

Testovací organizace aplikací

Testovací aplikace je konzolová aplikace uvnitř tests/RazorPagesProject.Tests adresáře.

Testovací adresář aplikace Popis
AuthTests Obsahuje testovací metody pro:
  • Přístup k zabezpečené stránce neověřeným uživatelem
  • Přístup ověřeného uživatele k zabezpečené stránce s využitím simulace AuthenticationHandler<TOptions>.
  • Získání profilu uživatele GitHubu a kontrola přihlášení uživatele profilu
BasicTests Obsahuje testovací metodu pro směrování a typ obsahu.
IntegrationTests Obsahuje integrační testy stránky indexu pomocí vlastní WebApplicationFactory třídy.
Helpers/Utilities
  • Utilities.cs obsahuje metodu použitou InitializeDbForTests k vytvoření databáze s testovacími daty.
  • HtmlHelpers.cs poskytuje metodu, která vrací AngleSharp IHtmlDocument pro použití testovacími metodami.
  • HttpClientExtensions.cs poskytuje přetížení pro SendAsync k odesílání požadavků do SUT.

Testovací architektura je xUnit. Integrační testy se provádějí pomocí Microsoft.AspNetCore.TestHost, která zahrnuje TestServer. Vzhledem k tomu, že se Microsoft.AspNetCore.Mvc.Testing balíček používá ke konfiguraci testovacího hostitele a testovacího serveru, TestHost nevyžadují a TestServer balíčky přímé odkazy na balíčky v souboru projektu testovací aplikace nebo konfiguraci vývojáře v testovací aplikaci.

Integrační testy obvykle před spuštěním testu vyžadují malou datovou sadu v databázi. Například test mazání vyžaduje odstranění záznamu z databáze, takže je třeba, aby databáze měla alespoň jeden záznam, aby žádost o odstranění byla úspěšná.

Ukázková aplikace naplní databázi třemi zprávami v Utilities.cs, které testy mohou použít při spuštění:

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

Kontext databáze SUT je registrován v jeho Startup.ConfigureServices metodě. Zpětné volání testovací aplikace builder.ConfigureServices se spustí po spuštění kódu aplikace Startup.ConfigureServices . Chcete-li pro testy použít jinou databázi, musí být kontext databáze aplikace nahrazen v builder.ConfigureServices. Další informace najdete v části Přizpůsobení WebApplicationFactory.

U jednotek SUT, které stále používají webového hostitele, se zpětné volání testovací aplikace builder.ConfigureServices provede před kódem Startup.ConfigureServices SUT. Zpětná volání testovací aplikace builder.ConfigureTestServices se spustí po.

Další materiály

Tento článek předpokládá základní znalost jednotkových testů. Pokud neznáte koncepty testů, podívejte se na článek o testování částí v .NET Core a .NET Standard a jeho propojeném obsahu.

Zobrazení nebo stažení ukázkového kódu (postup stažení)

Ukázková aplikace je aplikace Razor Pages a předpokládá základní znalost Razor Pages. Pokud Razor Pages neznáte, přečtěte si následující články:

Pro testování SPA doporučujeme nástroj, jako je Playwright pro .NET, který dokáže automatizovat prohlížeč.

Úvod do integračních testů

Integrační testy vyhodnocují komponenty aplikace na širší úrovni než testy jednotek. Testy jednotek se používají k testování izolovaných komponent softwaru, jako jsou jednotlivé metody tříd. Testy integrace ověřují, že dvě nebo více součástí aplikace spolupracují a vytvářejí očekávaný výsledek, a to včetně všech komponent potřebných k úplnému zpracování požadavku.

Tyto širší testy se používají k otestování infrastruktury a celé architektury aplikace, často včetně následujících komponent:

  • Databáze
  • Systém souborů
  • Síťová zařízení
  • Kanál odpovědi požadavku

Testy jednotek používají fiktivní komponenty, označované jako falešné objekty nebo mock objekty, místo komponent infrastruktury.

Na rozdíl od jednotkových testů jsou testy integrační:

  • Použijte skutečné komponenty, které aplikace používá v produkčním prostředí.
  • Vyžaduje více kódu a zpracování dat.
  • Bude to trvat déle.

Proto omezte použití integračních testů na nejdůležitější scénáře infrastruktury. Pokud je možné testovat chování pomocí testu jednotek nebo integračního testu, zvolte test jednotek.

V diskusích o integračních testech se testovaný projekt často nazývá systém pod testem nebo "SUT". "SUT" se používá v tomto článku k odkazování na aplikaci ASP.NET Core, která se testuje.

Nezapisujte integrační testy pro každou permutaci přístupu k datům a souborům v databázích a souborových systémech. Bez ohledu na to, kolik míst v aplikaci interaguje s databázemi a souborovými systémy, obvykle stačí zaměřená sada testů integrace pro čtení, zápis, aktualizaci a odstranění k dostatečnému testování komponentů databáze a souborového systému. Pro rutinní testy logiky metody, které s těmito komponentami pracují, použijte testy jednotek. Při testování jednotek může použití falešných nebo napodobených infrastruktur vést k rychlejšímu provádění testů.

ASP.NET Core integrační testy

Integrační testy v ASP.NET Core vyžadují následující:

  • Projekt testů se používá k zahrnutí a provádění testů. Testovací projekt má odkaz na SUT.
  • Testovací projekt vytvoří testovacího webového hostitele pro SUT a použije klienta testovacího serveru ke zpracování požadavků a odpovědí pomocí SUT.
  • Spouštěč testů se používá k provádění testů a hlášení výsledků testu.

Integrační testy se řídí posloupností událostí, které zahrnují obvyklé testovací kroky Uspořádat, Jednat a Assert :

  1. Webový hostitel SUT je nakonfigurovaný.
  2. Vytvoří se testovací serverový klient pro odesílání požadavků do aplikace.
  3. Je spuštěn testovací krok Příprava: Testovací aplikace připraví požadavek.
  4. Testovací krok Act je spuštěn: Klient odešle požadavek a obdrží odpověď.
  5. Provede se testovací krok Assert: Skutečná odpověď se ověří jako úspěšná nebo neúspěšná podle očekávané odpovědi.
  6. Proces pokračuje, dokud nejsou všechny testy provedeny.
  7. Výsledky testu jsou hlášeny.

Obvykle je testovací webový hostitel nakonfigurovaný jinak než normální webový hostitel aplikace pro testovací běhy. Pro testy se například může použít jiná databáze nebo jiná nastavení aplikace.

Komponenty infrastruktury, jako je testovací webový hostitel a testovací server v paměti (TestServer), jsou poskytovány nebo spravovány balíčkem Microsoft.AspNetCore.Mvc.Testing . Použití tohoto balíčku zjednodušuje vytváření a spouštění testů.

Balíček Microsoft.AspNetCore.Mvc.Testing zpracovává následující úlohy:

  • Zkopíruje soubor závislostí (.deps) z SUT do adresáře testovacího bin projektu.
  • Nastaví kořen obsahu na kořen projektu SUT tak, aby při spuštění testů byly statické soubory a stránky/zobrazení dostupné.
  • Poskytuje třídu WebApplicationFactory pro zjednodušení inicializace SUT pomocí .

Dokumentace k testům jednotek popisuje, jak nastavit testovací projekt a spouštěč testů, spolu s podrobnými pokyny ke spouštění testů a doporučení pro pojmenování testů a tříd testů.

Oddělte testy jednotek od integračních testů do různých projektů. Oddělení testů:

  • Pomáhá zajistit, aby součásti testování infrastruktury nejsou omylem zahrnuty do testů jednotek.
  • Umožňuje kontrolu nad tím, jakou sadu testů se spouští.

Konfigurace pro testy Razor aplikací Pages a aplikací MVC není prakticky nijak rozdílná. Jediný rozdíl spočívá v tom, jak jsou testy pojmenovány. Razor V aplikaci Pages jsou testy koncových bodů stránky obvykle pojmenovány za třídou modelu stránky (například IndexPageTests pro testování integrace součástí pro indexovou stránku). V aplikaci MVC jsou testy obvykle uspořádány podle tříd kontroleru a pojmenovány podle kontrolerů, které testují (například HomeControllerTests pro testování integrace součástí pro Home kontroler).

Požadavky na testovací aplikaci

Testovací projekt musí:

Tyto požadavky se dají zobrazit v ukázkové aplikaci. tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj Zkontrolujte soubor. Ukázková aplikace používá testovací architekturu xUnit a knihovnu analyzátoru AngleSharp , takže ukázková aplikace také odkazuje:

V aplikacích, které používají xunit.runner.visualstudio verzi 2.4.2 nebo novější, musí testovací projekt odkazovat na Microsoft.NET.Test.Sdk balíček.

Entity Framework Core se také používá v testech. Prohlédněte si soubor projektu na GitHubu.

Prostředí SUT

Pokud prostředí SUT není nastavené, prostředí se ve výchozím nastavení nastaví na Vývoj.

Základní testy s výchozím WebApplicationFactory

Implicitně definovanou Program třídu zpřístupňte testovacímu projektu jedním z následujících způsobů:

  • Zveřejnění interních typů z webové aplikace do testovacího projektu To lze provést v souboru projektu SUT (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Program Nastavení třídy jako veřejné pomocí částečné deklarace třídy:

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

    Ukázková aplikace používá přístup částečné Program třídy.

WebApplicationFactory<TEntryPoint> slouží k vytvoření TestServer pro integrační testy. TEntryPoint je vstupní bod třídy SUT, obvykle Program.cs.

Třídy testů implementují rozhraní třídní fixatury () aby se označilo, že třída obsahuje testy, a poskytují sdílené instance objektů pro testy ve třídě.

Následující testovací třída BasicTests používá WebApplicationFactory ke spuštění SUT a poskytuje HttpClient testovací metodě Get_EndpointsReturnSuccessAndCorrectContentType. Metoda ověří, že stavový kód odpovědi je úspěšný (200–299) a hlavička Content-Type je text/html; charset=utf-8 pro několik stránek aplikace.

CreateClient() vytvoří instanci HttpClient , která automaticky sleduje přesměrování a zpracovává soubory cookie.

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

Ve výchozím nastavení se soubory cookie, které nejsou nezbytné, nezachovávají mezi požadavky, když jsou povoleny zásady souhlasu podle Obecného nařízení o ochraně osobních údajů (GDPR). Pokud chcete zachovat soubory cookie, které nejsou nezbytné, například soubory používané poskytovatelem TempData, označte je v testech jako nezbytné. Pokyny k označení cookie jako nezbytné naleznete v části Základní soubory cookie.

AngleSharp vs Application Parts pro antiforgery ověření

Tento článek používá analyzátor AngleSharp ke zpracování kontrol proti padělání tím, že načítá stránky a analyzuje HTML. Pokud chcete otestovat koncové body zobrazení kontroleru a Razor stránek na nižší úrovni, aniž byste se museli zabývat tím, jak se vykreslují v prohlížeči, zvažte použití Application Parts. Přístup Application Parts vloží kontroler nebo Razor stránku do aplikace, což lze použít k odesílání JSON požadavků pro získání potřebných hodnot. Další informace najdete v blogu o testování integrace prostředků ASP.NET Core chráněných antiforgery pomocí částí aplikace a přidruženém úložišti GitHub, od Martina Costella.

Přizpůsobení služby WebApplicationFactory

Konfiguraci webového hostitele lze vytvořit nezávisle na testovacích třídách tím, že zdědíte z WebApplicationFactory<TEntryPoint>, abyste vytvořili jednu nebo více vlastních továren:

  1. Dědit z WebApplicationFactory a přepsat ConfigureWebHost. Tento prvek IWebHostBuilder umožňuje konfiguraci kolekce služeb pomocí 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");
        }
    }
    

    Naplnění databáze v ukázkové aplikaci se provádí metodou InitializeDbForTests. Metoda je popsána v sekci Ukázka integračních testů: Organizace testovací aplikace.

    Kontext databáze SUT je registrován v Program.cs. Zpětné volání testovací aplikace builder.ConfigureServices se spustí po spuštění kódu aplikace Program.cs . Chcete-li použít jinou databázi pro testy, než je databáze aplikace, musí být kontext databáze aplikace nahrazen v builder.ConfigureServices.

    Ukázková aplikace najde popisovač služby pro kontext databáze a pomocí popisovače odebere registraci služby. Továrna pak přidá nový ApplicationDbContext, který využívá databázi v paměti pro testování.

    Pokud se chcete připojit k jiné databázi, změňte soubor DbConnection. Použití testovací databáze SQL Serveru:

  1. Použijte vlastní CustomWebApplicationFactory v testovacích třídách. Příklad níže používá továrnu ve třídě 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
            });
        }
    

    Klient ukázkové aplikace je nakonfigurovaný tak, aby zabránil HttpClient následujícím přesměrováním. Jak je vysvětleno později v části Napodobení ověřování , umožňuje testům zkontrolovat výsledek první odpovědi aplikace. První odpověď v mnoha těchto testech je přesměrování s hlavičkou Location.

  2. Typický test používá HttpClient metody a pomocné metody ke zpracování požadavku a odpovědi:

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

Všechny požadavky POST na SUT musí splňovat antiforgery kontrolu, která je automaticky provedena systémem ochrany dat aplikace. Aby bylo možné zajistit požadavek POST testu, musí testovací aplikace:

  1. Vytvořte žádost o stránku.
  2. Parsujte antiforgery token cookie a token pro ověření požadavku z odpovědi.
  3. Vytvořte žádost POST s použitím antiforgery cookie a ověřovacího tokenu žádosti.

Pomocné SendAsync rozšiřující metody (Helpers/HttpClientExtensions.cs) a GetDocumentAsync pomocná metoda (Helpers/HtmlHelpers.cs) v ukázkové aplikaci používají analyzátor AngleSharp ke zpracování kontroly antiforgery následujícími metodami:

  • GetDocumentAsync: Přijme HttpResponseMessage a vrátí hodnotu IHtmlDocument. GetDocumentAsync používá továrnu, která připraví virtuální odpověď na základě původní HttpResponseMessage. Další informace najdete v dokumentaci AngleSharp.
  • SendAsync metody rozšíření pro HttpClient vytvoření žádosti HttpRequestMessage a volání SendAsync(HttpRequestMessage) k odeslání žádostí do SUT. Přetížení pro SendAsync přijímají HTML formulář (IHtmlFormElement) a následující:
    • Tlačítko Odeslat formulář (IHtmlElement)
    • Kolekce hodnot formulářů (IEnumerable<KeyValuePair<string, string>>)
    • Tlačítko Odeslat (IHtmlElement) a hodnoty formuláře (IEnumerable<KeyValuePair<string, string>>)

AngleSharp je knihovna analýzy třetí strany používaná pro demonstrační účely v tomto článku a ukázkové aplikaci. AngleSharp se nepodporuje ani nevyžaduje pro testování integrace aplikací ASP.NET Core. Dalšími analyzátory, které lze použít, je například Html Agility Pack (HAP). Dalším přístupem je napsat kód pro zpracování ověřovacího tokenu, který systém antiforgery používá pro ověřování žádostí, a řídit antiforgery cookie přímo. Další informace najdete v části AngleSharp vs Application Parts kontroly antiforgery v tomto článku.

Poskytovatel databáze EF-Core v paměti lze použít pro omezené a základní testování, avšak pro testování v paměti se doporučuje poskytovatel SQLite.

Viz Rozšíření spuštění pomocí startovacích filtrů, který ukazuje, jak nakonfigurovat middleware pomocí IStartupFilter, což je užitečné, když test vyžaduje vlastní službu nebo middleware.

Přizpůsobení klienta pomocí withWebHostBuilder

Pokud je v rámci testovací metody vyžadována další konfigurace, WithWebHostBuilder vytvoří novou WebApplicationFactory s IWebHostBuilder, která je dále přizpůsobena pomocí konfigurace.

Ukázkový kód volá WithWebHostBuilder, aby nahradil nakonfigurované služby testovacími zástupci. Další informace a příklady použití najdete v části Vložení napodobených služeb v tomto článku.

Metoda Post_DeleteMessageHandler_ReturnsRedirectToRoot testu ukázkové aplikace ukazuje použití WithWebHostBuilder. Tento test provede odstranění záznamu v databázi aktivací odeslání formuláře v SUT.

Protože jiný test ve IndexPageTests třídě provádí operaci, která odstraní všechny záznamy v databázi a může běžet před Post_DeleteMessageHandler_ReturnsRedirectToRoot metodou, databáze bude znovu provedena v této testovací metodě, aby se zajistilo, že záznam existuje pro SUT k odstranění. Výběr prvního tlačítka messages pro odstranění formuláře v SUT se simuluje v požadavku na 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);
}

Možnosti klienta

Podívejte se na WebApplicationFactoryClientOptions stránku s výchozími a dostupnými možnostmi při vytváření HttpClient instancí.

WebApplicationFactoryClientOptions Vytvořte třídu a předejte ji metodě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
        });
    }

POZNÁMKA: Pokud se chcete vyhnout upozorněním přesměrování HTTPS v protokolech při použití middlewaru přesměrování HTTPS, nastavte BaseAddress = new Uri("https://localhost")

Vložte simulované služby

Služby je možné přepsat v testu voláním ConfigureTestServices na builderu hostitele. K určení rozsahu přepsaných služeb na samotný test se používá metoda WithWebHostBuilder k načtení host builderu. To lze vidět v následujících testech:

Příklad SUT obsahuje vymezenou službu, která vrací citát. Citace je vložena do skrytého pole na stránce Index, když je stránka Index žádána.

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

Při spuštění aplikace SUT se vygeneruje následující kód:

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

K otestování injektáže služby a nabídky v integračním testu se do testu vloží napodobená služba. Služba napodobení nahradí aplikaci QuoteService službou poskytovanou testovací aplikací, která se nazývá 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 je volána a služba s vymezeným oborem je zaregistrovaná:

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

Kód vytvořený během provádění testu odráží uvozovkový text zadaný TestQuoteService, a proto kontrola projde:

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

Simulované ověřování

Testy ve AuthTests třídě kontrolují, že zabezpečený koncový bod:

  • Přesměruje neověřeného uživatele na přihlašovací stránku aplikace.
  • Vrátí obsah pro ověřeného uživatele.

V SUT stránka /SecurePage používá AuthorizePage konvenci pro aplikaci AuthorizeFilter na stránku. Další informace najdete v tématu Razor Konvence autorizace stránek.

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

V testu Get_SecurePageRedirectsAnUnauthenticatedUser je možnost WebApplicationFactoryClientOptions nastavena tak, aby zakázala přesměrování nastavením AllowAutoRedirect na 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);
}

Když zakážete klientovi sledovat přesměrování, můžete provést následující kontroly:

  • Stavový kód vrácený SUT lze zkontrolovat podle očekávaného HttpStatusCode.Redirect výsledku, nikoli konečný stavový kód po přesměrování na přihlašovací stránku, což by bylo HttpStatusCode.OK.
  • Hodnota záhlaví Location v hlavičkách odpovědi se kontroluje, aby se ověřilo, že začíná na http://localhost/Identity/Account/Login, nikoli na konečné odpovědi přihlašovací stránky, kde by záhlaví Location nebylo k dispozici.

Testovací aplikace může simulovat AuthenticationHandler<TOptions> v ConfigureTestServices za účelem testování aspektů ověřování a autorizace. Minimální scénář vrátí 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);
    }
}

Když je schéma ověřování nastaveno na TestAuthHandler, volá se TestScheme k ověření uživatele, kde AddAuthentication je registrován pro ConfigureTestServices. Je důležité, TestScheme aby schéma odpovídalo schématu, které vaše aplikace očekává. Jinak ověřování nebude fungovat.

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

Další informace o WebApplicationFactoryClientOptionsnaleznete v části možnosti klienta.

Základní testy pro ověřovací middleware

Základní testy middlewaru ověřování najdete v tomto úložišti GitHubu. Obsahuje testovací server specifický pro testovací scénář.

Nastavení prostředí

Nastavte prostředí ve vlastní objektu pro vytváření aplikací:

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

Způsob, jakým testovací infrastruktura odvodí kořenovou cestu obsahu aplikace

Konstruktor WebApplicationFactory odvodí kořenovou cestu obsahu aplikace tak, že vyhledá WebApplicationFactoryContentRootAttribute v sestavení obsahující integrační testy s klíčem, který se rovná TEntryPoint sestavení System.Reflection.Assembly.FullName. Pokud se atribut se správným klíčem nenajde, WebApplicationFactory vrátí se zpět do hledání souboru řešení (.sln) a připojí TEntryPoint název sestavení do adresáře řešení. Kořenový adresář aplikace (kořenová cesta k obsahu) slouží ke zjišťování zobrazení a souborů obsahu.

Zakázat stínové kopírování

Stínová kopie způsobí, že testy se spustí v jiném adresáři než ve výstupním adresáři. Pokud vaše testy spoléhají na načítání souborů vzhledem k Assembly.Location a dochází k problémům, možná budete muset zakázat stínové kopírování.

Pokud chcete zakázat stínové kopírování při použití xUnit, vytvořte v adresáři testovacího xunit.runner.json projektu soubor se správným nastavením konfigurace:

{
  "shadowCopy": false
}

Likvidace objektů

Po provedení testů implementace IClassFixture jsou TestServer a HttpClient uvolněny, když xUnit uvolní WebApplicationFactory. Pokud instance objektů vytvořených vývojářem vyžadují odstranění, odstraňte je v implementaci IClassFixture. Další informace naleznete v tématu Implementace metody Dispose.

Ukázka integračních testů

Ukázková aplikace se skládá ze dvou aplikací:

Aplikace Adresář projektu Popis
Aplikace zpráv (SUT) src/RazorPagesProject Umožňuje uživateli přidávat, odstraňovat, odstraňovat všechny a analyzovat zprávy.
Testovací aplikace tests/RazorPagesProject.Tests Používá se k provádění integračního testování SUT.

Testy je možné spouštět pomocí integrovaných testovacích funkcí IDE, například Visual Studio. Pokud používáte Visual Studio Code nebo příkazový řádek, spusťte v adresáři tests/RazorPagesProject.Tests následující příkaz:

dotnet test

Organizace aplikace zpráv (SUT)

SUT je Razor systém zpráv Pages s následujícími vlastnostmi:

  • Indexová stránka aplikace (Pages/Index.cshtml a Pages/Index.cshtml.cs) poskytuje uživatelské rozhraní a metody modelu stránky pro řízení sčítání, odstranění a analýzy zpráv (průměrná slova na zprávu).
  • Zpráva je popsána Message třídou (Data/Message.cs) se dvěma vlastnostmi: Id (klíč) a Text (zpráva). Vlastnost Text je povinná a omezená na 200 znaků.
  • Zprávy se ukládají pomocí databáze v paměti entity Framework†.
  • Aplikace obsahuje vrstvu přístupu k datům (DAL) ve své třídě AppDbContext kontextu databáze (Data/AppDbContext.cs).
  • Pokud je databáze při spuštění aplikace prázdná, úložiště zpráv se inicializuje se třemi zprávami.
  • Aplikace zahrnuje /SecurePage, ke kterému má přístup pouze ověřený uživatel.

Článek EF, Test s InMemory vysvětluje, jak používat in-memory databázi pro testování s MSTest. Toto téma používá testovací architekturu xUnit . Koncepty testů a implementace testů v různých testovacích architekturách jsou podobné, ale ne identické.

I když aplikace nepoužívá vzor úložiště a není efektivním příkladem Unit of Work (UoW) pattern, stránky podporují tyto vzory vývoje. Další informace najdete v tématu Návrh vrstvy trvalosti infrastruktury a logiky kontroleru testů (ukázka implementuje vzor úložiště).

Testovací organizace aplikací

Testovací aplikace je konzolová aplikace uvnitř tests/RazorPagesProject.Tests adresáře.

Testovací adresář aplikace Popis
AuthTests Obsahuje testovací metody pro:
  • Přístup k zabezpečené stránce neověřeným uživatelem
  • Přístup ověřeného uživatele k zabezpečené stránce s využitím simulace AuthenticationHandler<TOptions>.
  • Získání profilu uživatele GitHubu a kontrola přihlášení uživatele profilu
BasicTests Obsahuje testovací metodu pro směrování a typ obsahu.
IntegrationTests Obsahuje integrační testy stránky indexu pomocí vlastní WebApplicationFactory třídy.
Helpers/Utilities
  • Utilities.cs obsahuje metodu použitou InitializeDbForTests k vytvoření databáze s testovacími daty.
  • HtmlHelpers.cs poskytuje metodu, která vrací AngleSharp IHtmlDocument pro použití testovacími metodami.
  • HttpClientExtensions.cs poskytuje přetížení pro SendAsync k odesílání požadavků do SUT.

Testovací architektura je xUnit. Integrační testy se provádějí pomocí Microsoft.AspNetCore.TestHost, která zahrnuje TestServer. Vzhledem k tomu, že se Microsoft.AspNetCore.Mvc.Testing balíček používá ke konfiguraci testovacího hostitele a testovacího serveru, TestHost nevyžadují a TestServer balíčky přímé odkazy na balíčky v souboru projektu testovací aplikace nebo konfiguraci vývojáře v testovací aplikaci.

Integrační testy obvykle před spuštěním testu vyžadují malou datovou sadu v databázi. Například test mazání vyžaduje odstranění záznamu z databáze, takže je třeba, aby databáze měla alespoň jeden záznam, aby žádost o odstranění byla úspěšná.

Ukázková aplikace naplní databázi třemi zprávami v Utilities.cs, které testy mohou použít při spuštění:

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

Kontext databáze SUT je registrován v Program.cs. Zpětné volání testovací aplikace builder.ConfigureServices se spustí po spuštění kódu aplikace Program.cs . Chcete-li pro testy použít jinou databázi, musí být kontext databáze aplikace nahrazen v builder.ConfigureServices. Další informace najdete v části Přizpůsobení WebApplicationFactory.

Další materiály

Tento článek předpokládá základní znalost jednotkových testů. Pokud neznáte koncepty testů, podívejte se na článek o testování částí v .NET Core a .NET Standard a jeho propojeném obsahu.

Zobrazení nebo stažení ukázkového kódu (postup stažení)

Ukázková aplikace je aplikace Razor Pages a předpokládá základní znalost Razor Pages. Pokud Razor Pages neznáte, přečtěte si následující články:

Pro testování SPA doporučujeme nástroj, jako je Playwright pro .NET, který dokáže automatizovat prohlížeč.

Úvod do integračních testů

Integrační testy vyhodnocují komponenty aplikace na širší úrovni než testy jednotek. Testy jednotek se používají k testování izolovaných komponent softwaru, jako jsou jednotlivé metody tříd. Testy integrace ověřují, že dvě nebo více součástí aplikace spolupracují a vytvářejí očekávaný výsledek, a to včetně všech komponent potřebných k úplnému zpracování požadavku.

Tyto širší testy se používají k otestování infrastruktury a celé architektury aplikace, často včetně následujících komponent:

  • Databáze
  • Systém souborů
  • Síťová zařízení
  • Kanál odpovědi požadavku

Testy jednotek používají fiktivní komponenty, označované jako falešné objekty nebo mock objekty, místo komponent infrastruktury.

Na rozdíl od jednotkových testů jsou testy integrační:

  • Použijte skutečné komponenty, které aplikace používá v produkčním prostředí.
  • Vyžaduje více kódu a zpracování dat.
  • Bude to trvat déle.

Proto omezte použití integračních testů na nejdůležitější scénáře infrastruktury. Pokud je možné testovat chování pomocí testu jednotek nebo integračního testu, zvolte test jednotek.

V diskusích o integračních testech se testovaný projekt často nazývá systém pod testem nebo "SUT". "SUT" se používá v tomto článku k odkazování na aplikaci ASP.NET Core, která se testuje.

Nezapisujte integrační testy pro každou permutaci přístupu k datům a souborům v databázích a souborových systémech. Bez ohledu na to, kolik míst v aplikaci interaguje s databázemi a souborovými systémy, obvykle stačí zaměřená sada testů integrace pro čtení, zápis, aktualizaci a odstranění k dostatečnému testování komponentů databáze a souborového systému. Pro rutinní testy logiky metody, které s těmito komponentami pracují, použijte testy jednotek. Při testování jednotek může použití falešných nebo napodobených infrastruktur vést k rychlejšímu provádění testů.

ASP.NET Core integrační testy

Integrační testy v ASP.NET Core vyžadují následující:

  • Projekt testů se používá k zahrnutí a provádění testů. Testovací projekt má odkaz na SUT.
  • Testovací projekt vytvoří testovacího webového hostitele pro SUT a použije klienta testovacího serveru ke zpracování požadavků a odpovědí pomocí SUT.
  • Spouštěč testů se používá k provádění testů a hlášení výsledků testu.

Integrační testy se řídí posloupností událostí, které zahrnují obvyklé testovací kroky Uspořádat, Jednat a Assert :

  1. Webový hostitel SUT je nakonfigurovaný.
  2. Vytvoří se testovací serverový klient pro odesílání požadavků do aplikace.
  3. Je spuštěn testovací krok Příprava: Testovací aplikace připraví požadavek.
  4. Testovací krok Act je spuštěn: Klient odešle požadavek a obdrží odpověď.
  5. Provede se testovací krok Assert: Skutečná odpověď se ověří jako úspěšná nebo neúspěšná podle očekávané odpovědi.
  6. Proces pokračuje, dokud nejsou všechny testy provedeny.
  7. Výsledky testu jsou hlášeny.

Obvykle je testovací webový hostitel nakonfigurovaný jinak než normální webový hostitel aplikace pro testovací běhy. Pro testy se například může použít jiná databáze nebo jiná nastavení aplikace.

Komponenty infrastruktury, jako je testovací webový hostitel a testovací server v paměti (TestServer), jsou poskytovány nebo spravovány balíčkem Microsoft.AspNetCore.Mvc.Testing . Použití tohoto balíčku zjednodušuje vytváření a spouštění testů.

Balíček Microsoft.AspNetCore.Mvc.Testing zpracovává následující úlohy:

  • Zkopíruje soubor závislostí (.deps) z SUT do adresáře testovacího bin projektu.
  • Nastaví kořen obsahu na kořen projektu SUT tak, aby při spuštění testů byly statické soubory a stránky/zobrazení dostupné.
  • Poskytuje třídu WebApplicationFactory pro zjednodušení inicializace SUT pomocí .

Dokumentace k testům jednotek popisuje, jak nastavit testovací projekt a spouštěč testů, spolu s podrobnými pokyny ke spouštění testů a doporučení pro pojmenování testů a tříd testů.

Oddělte testy jednotek od integračních testů do různých projektů. Oddělení testů:

  • Pomáhá zajistit, aby součásti testování infrastruktury nejsou omylem zahrnuty do testů jednotek.
  • Umožňuje kontrolu nad tím, jakou sadu testů se spouští.

Konfigurace pro testy Razor aplikací Pages a aplikací MVC není prakticky nijak rozdílná. Jediný rozdíl spočívá v tom, jak jsou testy pojmenovány. Razor V aplikaci Pages jsou testy koncových bodů stránky obvykle pojmenovány za třídou modelu stránky (například IndexPageTests pro testování integrace součástí pro indexovou stránku). V aplikaci MVC jsou testy obvykle uspořádány podle tříd kontroleru a pojmenovány podle kontrolerů, které testují (například HomeControllerTests pro testování integrace součástí pro Home kontroler).

Požadavky na testovací aplikaci

Testovací projekt musí:

Tyto požadavky se dají zobrazit v ukázkové aplikaci. tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj Zkontrolujte soubor. Ukázková aplikace používá testovací architekturu xUnit a knihovnu analyzátoru AngleSharp , takže ukázková aplikace také odkazuje:

V aplikacích, které používají xunit.runner.visualstudio verzi 2.4.2 nebo novější, musí testovací projekt odkazovat na Microsoft.NET.Test.Sdk balíček.

Entity Framework Core se také používá v testech. Prohlédněte si soubor projektu na GitHubu.

Prostředí SUT

Pokud prostředí SUT není nastavené, prostředí se ve výchozím nastavení nastaví na Vývoj.

Základní testy s výchozím WebApplicationFactory

Implicitně definovanou Program třídu zpřístupňte testovacímu projektu jedním z následujících způsobů:

  • Zveřejnění interních typů z webové aplikace do testovacího projektu To lze provést v souboru projektu SUT (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Program Nastavení třídy jako veřejné pomocí částečné deklarace třídy:

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

    Ukázková aplikace používá přístup částečné Program třídy.

WebApplicationFactory<TEntryPoint> slouží k vytvoření TestServer pro integrační testy. TEntryPoint je vstupní bod třídy SUT, obvykle Program.cs.

Třídy testů implementují rozhraní třídní fixatury () aby se označilo, že třída obsahuje testy, a poskytují sdílené instance objektů pro testy ve třídě.

Následující testovací třída BasicTests používá WebApplicationFactory ke spuštění SUT a poskytuje HttpClient testovací metodě Get_EndpointsReturnSuccessAndCorrectContentType. Metoda ověří, že stavový kód odpovědi je úspěšný (200–299) a hlavička Content-Type je text/html; charset=utf-8 pro několik stránek aplikace.

CreateClient() vytvoří instanci HttpClient , která automaticky sleduje přesměrování a zpracovává soubory cookie.

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

Ve výchozím nastavení se soubory cookie, které nejsou nezbytné, nezachovávají mezi požadavky, když jsou povoleny zásady souhlasu podle Obecného nařízení o ochraně osobních údajů (GDPR). Pokud chcete zachovat soubory cookie, které nejsou nezbytné, například soubory používané poskytovatelem TempData, označte je v testech jako nezbytné. Pokyny k označení cookie jako nezbytné naleznete v části Základní soubory cookie.

AngleSharp vs Application Parts pro antiforgery ověření

Tento článek používá analyzátor AngleSharp ke zpracování kontrol proti padělání tím, že načítá stránky a analyzuje HTML. Pokud chcete otestovat koncové body zobrazení kontroleru a Razor stránek na nižší úrovni, aniž byste se museli zabývat tím, jak se vykreslují v prohlížeči, zvažte použití Application Parts. Přístup Application Parts vloží kontroler nebo Razor stránku do aplikace, což lze použít k odesílání JSON požadavků pro získání potřebných hodnot. Další informace najdete v blogu o testování integrace prostředků ASP.NET Core chráněných antiforgery pomocí částí aplikace a přidruženém úložišti GitHub, od Martina Costella.

Přizpůsobení služby WebApplicationFactory

Konfiguraci webového hostitele lze vytvořit nezávisle na testovacích třídách tím, že zdědíte z WebApplicationFactory<TEntryPoint>, abyste vytvořili jednu nebo více vlastních továren:

  1. Dědit z WebApplicationFactory a přepsat ConfigureWebHost. Tento prvek IWebHostBuilder umožňuje konfiguraci kolekce služeb pomocí 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");
        }
    }
    

    Naplnění databáze v ukázkové aplikaci se provádí metodou InitializeDbForTests. Metoda je popsána v sekci Ukázka integračních testů: Organizace testovací aplikace.

    Kontext databáze SUT je registrován v Program.cs. Zpětné volání testovací aplikace builder.ConfigureServices se spustí po spuštění kódu aplikace Program.cs . Chcete-li použít jinou databázi pro testy, než je databáze aplikace, musí být kontext databáze aplikace nahrazen v builder.ConfigureServices.

    Ukázková aplikace najde popisovač služby pro kontext databáze a pomocí popisovače odebere registraci služby. Továrna pak přidá nový ApplicationDbContext, který využívá databázi v paměti pro testování.

    Pokud se chcete připojit k jiné databázi, změňte soubor DbConnection. Použití testovací databáze SQL Serveru:

  1. Použijte vlastní CustomWebApplicationFactory v testovacích třídách. Příklad níže používá továrnu ve třídě 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
            });
        }
    }
    

    Klient ukázkové aplikace je nakonfigurovaný tak, aby zabránil HttpClient následujícím přesměrováním. Jak je vysvětleno později v části Napodobení ověřování , umožňuje testům zkontrolovat výsledek první odpovědi aplikace. První odpověď v mnoha těchto testech je přesměrování s hlavičkou Location.

  2. Typický test používá HttpClient metody a pomocné metody ke zpracování požadavku a odpovědi:

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

Všechny požadavky POST na SUT musí splňovat antiforgery kontrolu, která je automaticky provedena systémem ochrany dat aplikace. Aby bylo možné zajistit požadavek POST testu, musí testovací aplikace:

  1. Vytvořte žádost o stránku.
  2. Parsujte antiforgery token cookie a token pro ověření požadavku z odpovědi.
  3. Vytvořte žádost POST s použitím antiforgery cookie a ověřovacího tokenu žádosti.

Pomocné SendAsync rozšiřující metody (Helpers/HttpClientExtensions.cs) a GetDocumentAsync pomocná metoda (Helpers/HtmlHelpers.cs) v ukázkové aplikaci používají analyzátor AngleSharp ke zpracování kontroly antiforgery následujícími metodami:

  • GetDocumentAsync: Přijme HttpResponseMessage a vrátí hodnotu IHtmlDocument. GetDocumentAsync používá továrnu, která připraví virtuální odpověď na základě původní HttpResponseMessage. Další informace najdete v dokumentaci AngleSharp.
  • SendAsync metody rozšíření pro HttpClient vytvoření žádosti HttpRequestMessage a volání SendAsync(HttpRequestMessage) k odeslání žádostí do SUT. Přetížení pro SendAsync přijímají HTML formulář (IHtmlFormElement) a následující:
    • Tlačítko Odeslat formulář (IHtmlElement)
    • Kolekce hodnot formulářů (IEnumerable<KeyValuePair<string, string>>)
    • Tlačítko Odeslat (IHtmlElement) a hodnoty formuláře (IEnumerable<KeyValuePair<string, string>>)

AngleSharp je knihovna analýzy třetí strany používaná pro demonstrační účely v tomto článku a ukázkové aplikaci. AngleSharp se nepodporuje ani nevyžaduje pro testování integrace aplikací ASP.NET Core. Dalšími analyzátory, které lze použít, je například Html Agility Pack (HAP). Dalším přístupem je napsat kód pro zpracování ověřovacího tokenu, který systém antiforgery používá pro ověřování žádostí, a řídit antiforgery cookie přímo. Další informace najdete v části AngleSharp vs Application Parts kontroly antiforgery v tomto článku.

Poskytovatel databáze EF-Core v paměti lze použít pro omezené a základní testování, avšak pro testování v paměti se doporučuje poskytovatel SQLite.

Viz Rozšíření spuštění pomocí startovacích filtrů, který ukazuje, jak nakonfigurovat middleware pomocí IStartupFilter, což je užitečné, když test vyžaduje vlastní službu nebo middleware.

Přizpůsobení klienta pomocí withWebHostBuilder

Pokud je v rámci testovací metody vyžadována další konfigurace, WithWebHostBuilder vytvoří novou WebApplicationFactory s IWebHostBuilder, která je dále přizpůsobena pomocí konfigurace.

Ukázkový kód volá WithWebHostBuilder, aby nahradil nakonfigurované služby testovacími zástupci. Další informace a příklady použití najdete v části Vložení napodobených služeb v tomto článku.

Metoda Post_DeleteMessageHandler_ReturnsRedirectToRoot testu ukázkové aplikace ukazuje použití WithWebHostBuilder. Tento test provede odstranění záznamu v databázi aktivací odeslání formuláře v SUT.

Protože jiný test ve IndexPageTests třídě provádí operaci, která odstraní všechny záznamy v databázi a může běžet před Post_DeleteMessageHandler_ReturnsRedirectToRoot metodou, databáze bude znovu provedena v této testovací metodě, aby se zajistilo, že záznam existuje pro SUT k odstranění. Výběr prvního tlačítka messages pro odstranění formuláře v SUT se simuluje v požadavku na 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);
}

Možnosti klienta

Podívejte se na WebApplicationFactoryClientOptions stránku s výchozími a dostupnými možnostmi při vytváření HttpClient instancí.

WebApplicationFactoryClientOptions Vytvořte třídu a předejte ji metodě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
        });
    }
}

POZNÁMKA: Pokud se chcete vyhnout upozorněním přesměrování HTTPS v protokolech při použití middlewaru přesměrování HTTPS, nastavte BaseAddress = new Uri("https://localhost")

Vložte simulované služby

Služby je možné přepsat v testu voláním ConfigureTestServices na builderu hostitele. K určení rozsahu přepsaných služeb na samotný test se používá metoda WithWebHostBuilder k načtení host builderu. To lze vidět v následujících testech:

Příklad SUT obsahuje vymezenou službu, která vrací citát. Citace je vložena do skrytého pole na stránce Index, když je stránka Index žádána.

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

Při spuštění aplikace SUT se vygeneruje následující kód:

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

K otestování injektáže služby a nabídky v integračním testu se do testu vloží napodobená služba. Služba napodobení nahradí aplikaci QuoteService službou poskytovanou testovací aplikací, která se nazývá 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 je volána a služba s vymezeným oborem je zaregistrovaná:

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

Kód vytvořený během provádění testu odráží uvozovkový text zadaný TestQuoteService, a proto kontrola projde:

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

Simulované ověřování

Testy ve AuthTests třídě kontrolují, že zabezpečený koncový bod:

  • Přesměruje neověřeného uživatele na přihlašovací stránku aplikace.
  • Vrátí obsah pro ověřeného uživatele.

V SUT stránka /SecurePage používá AuthorizePage konvenci pro aplikaci AuthorizeFilter na stránku. Další informace najdete v tématu Razor Konvence autorizace stránek.

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

V testu Get_SecurePageRedirectsAnUnauthenticatedUser je možnost WebApplicationFactoryClientOptions nastavena tak, aby zakázala přesměrování nastavením AllowAutoRedirect na 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);
}

Když zakážete klientovi sledovat přesměrování, můžete provést následující kontroly:

  • Stavový kód vrácený SUT lze zkontrolovat podle očekávaného HttpStatusCode.Redirect výsledku, nikoli konečný stavový kód po přesměrování na přihlašovací stránku, což by bylo HttpStatusCode.OK.
  • Hodnota záhlaví Location v hlavičkách odpovědi se kontroluje, aby se ověřilo, že začíná na http://localhost/Identity/Account/Login, nikoli na konečné odpovědi přihlašovací stránky, kde by záhlaví Location nebylo k dispozici.

Testovací aplikace může simulovat AuthenticationHandler<TOptions> v ConfigureTestServices za účelem testování aspektů ověřování a autorizace. Minimální scénář vrátí 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);
    }
}

Když je schéma ověřování nastaveno na TestAuthHandler, volá se TestScheme k ověření uživatele, kde AddAuthentication je registrován pro ConfigureTestServices. Je důležité, TestScheme aby schéma odpovídalo schématu, které vaše aplikace očekává. Jinak ověřování nebude fungovat.

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

Další informace o WebApplicationFactoryClientOptionsnaleznete v části možnosti klienta.

Základní testy pro ověřovací middleware

Základní testy middlewaru ověřování najdete v tomto úložišti GitHubu. Obsahuje testovací server specifický pro testovací scénář.

Nastavení prostředí

Nastavte prostředí ve vlastní objektu pro vytváření aplikací:

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

Způsob, jakým testovací infrastruktura odvodí kořenovou cestu obsahu aplikace

Konstruktor WebApplicationFactory odvodí kořenovou cestu obsahu aplikace tak, že vyhledá WebApplicationFactoryContentRootAttribute v sestavení obsahující integrační testy s klíčem, který se rovná TEntryPoint sestavení System.Reflection.Assembly.FullName. Pokud se atribut se správným klíčem nenajde, WebApplicationFactory vrátí se zpět do hledání souboru řešení (.sln) a připojí TEntryPoint název sestavení do adresáře řešení. Kořenový adresář aplikace (kořenová cesta k obsahu) slouží ke zjišťování zobrazení a souborů obsahu.

Zakázat stínové kopírování

Stínová kopie způsobí, že testy se spustí v jiném adresáři než ve výstupním adresáři. Pokud vaše testy spoléhají na načítání souborů vzhledem k Assembly.Location a dochází k problémům, možná budete muset zakázat stínové kopírování.

Pokud chcete zakázat stínové kopírování při použití xUnit, vytvořte v adresáři testovacího xunit.runner.json projektu soubor se správným nastavením konfigurace:

{
  "shadowCopy": false
}

Likvidace objektů

Po provedení testů implementace IClassFixture jsou TestServer a HttpClient uvolněny, když xUnit uvolní WebApplicationFactory. Pokud instance objektů vytvořených vývojářem vyžadují odstranění, odstraňte je v implementaci IClassFixture. Další informace naleznete v tématu Implementace metody Dispose.

Ukázka integračních testů

Ukázková aplikace se skládá ze dvou aplikací:

Aplikace Adresář projektu Popis
Aplikace zpráv (SUT) src/RazorPagesProject Umožňuje uživateli přidávat, odstraňovat, odstraňovat všechny a analyzovat zprávy.
Testovací aplikace tests/RazorPagesProject.Tests Používá se k provádění integračního testování SUT.

Testy je možné spouštět pomocí integrovaných testovacích funkcí IDE, například Visual Studio. Pokud používáte Visual Studio Code nebo příkazový řádek, spusťte v adresáři tests/RazorPagesProject.Tests následující příkaz:

dotnet test

Organizace aplikace zpráv (SUT)

SUT je Razor systém zpráv Pages s následujícími vlastnostmi:

  • Indexová stránka aplikace (Pages/Index.cshtml a Pages/Index.cshtml.cs) poskytuje uživatelské rozhraní a metody modelu stránky pro řízení sčítání, odstranění a analýzy zpráv (průměrná slova na zprávu).
  • Zpráva je popsána Message třídou (Data/Message.cs) se dvěma vlastnostmi: Id (klíč) a Text (zpráva). Vlastnost Text je povinná a omezená na 200 znaků.
  • Zprávy se ukládají pomocí databáze v paměti entity Framework†.
  • Aplikace obsahuje vrstvu přístupu k datům (DAL) ve své třídě AppDbContext kontextu databáze (Data/AppDbContext.cs).
  • Pokud je databáze při spuštění aplikace prázdná, úložiště zpráv se inicializuje se třemi zprávami.
  • Aplikace zahrnuje /SecurePage, ke kterému má přístup pouze ověřený uživatel.

Článek EF, Test s InMemory vysvětluje, jak používat in-memory databázi pro testování s MSTest. Toto téma používá testovací architekturu xUnit . Koncepty testů a implementace testů v různých testovacích architekturách jsou podobné, ale ne identické.

I když aplikace nepoužívá vzor úložiště a není efektivním příkladem Unit of Work (UoW) pattern, stránky podporují tyto vzory vývoje. Další informace najdete v tématu Návrh vrstvy trvalosti infrastruktury a logiky kontroleru testů (ukázka implementuje vzor úložiště).

Testovací organizace aplikací

Testovací aplikace je konzolová aplikace uvnitř tests/RazorPagesProject.Tests adresáře.

Testovací adresář aplikace Popis
AuthTests Obsahuje testovací metody pro:
  • Přístup k zabezpečené stránce neověřeným uživatelem
  • Přístup ověřeného uživatele k zabezpečené stránce s využitím simulace AuthenticationHandler<TOptions>.
  • Získání profilu uživatele GitHubu a kontrola přihlášení uživatele profilu
BasicTests Obsahuje testovací metodu pro směrování a typ obsahu.
IntegrationTests Obsahuje integrační testy stránky indexu pomocí vlastní WebApplicationFactory třídy.
Helpers/Utilities
  • Utilities.cs obsahuje metodu použitou InitializeDbForTests k vytvoření databáze s testovacími daty.
  • HtmlHelpers.cs poskytuje metodu, která vrací AngleSharp IHtmlDocument pro použití testovacími metodami.
  • HttpClientExtensions.cs poskytuje přetížení pro SendAsync k odesílání požadavků do SUT.

Testovací architektura je xUnit. Integrační testy se provádějí pomocí Microsoft.AspNetCore.TestHost, která zahrnuje TestServer. Vzhledem k tomu, že se Microsoft.AspNetCore.Mvc.Testing balíček používá ke konfiguraci testovacího hostitele a testovacího serveru, TestHost nevyžadují a TestServer balíčky přímé odkazy na balíčky v souboru projektu testovací aplikace nebo konfiguraci vývojáře v testovací aplikaci.

Integrační testy obvykle před spuštěním testu vyžadují malou datovou sadu v databázi. Například test mazání vyžaduje odstranění záznamu z databáze, takže je třeba, aby databáze měla alespoň jeden záznam, aby žádost o odstranění byla úspěšná.

Ukázková aplikace naplní databázi třemi zprávami v Utilities.cs, které testy mohou použít při spuštění:

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

Kontext databáze SUT je registrován v Program.cs. Zpětné volání testovací aplikace builder.ConfigureServices se spustí po spuštění kódu aplikace Program.cs . Chcete-li pro testy použít jinou databázi, musí být kontext databáze aplikace nahrazen v builder.ConfigureServices. Další informace najdete v části Přizpůsobení WebApplicationFactory.

Další materiály

Tento článek předpokládá základní znalost jednotkových testů. Pokud neznáte koncepty testů, podívejte se na článek o testování částí v .NET Core a .NET Standard a jeho propojeném obsahu.

Zobrazení nebo stažení ukázkového kódu (postup stažení)

Ukázková aplikace je aplikace Razor Pages a předpokládá základní znalost Razor Pages. Pokud Razor Pages neznáte, přečtěte si následující články:

Pro testování SPA doporučujeme nástroj, jako je Playwright pro .NET, který dokáže automatizovat prohlížeč.

Úvod do integračních testů

Integrační testy vyhodnocují komponenty aplikace na širší úrovni než testy jednotek. Testy jednotek se používají k testování izolovaných komponent softwaru, jako jsou jednotlivé metody tříd. Testy integrace ověřují, že dvě nebo více součástí aplikace spolupracují a vytvářejí očekávaný výsledek, a to včetně všech komponent potřebných k úplnému zpracování požadavku.

Tyto širší testy se používají k otestování infrastruktury a celé architektury aplikace, často včetně následujících komponent:

  • Databáze
  • Systém souborů
  • Síťová zařízení
  • Kanál odpovědi požadavku

Testy jednotek používají fiktivní komponenty, označované jako falešné objekty nebo mock objekty, místo komponent infrastruktury.

Na rozdíl od jednotkových testů jsou testy integrační:

  • Použijte skutečné komponenty, které aplikace používá v produkčním prostředí.
  • Vyžaduje více kódu a zpracování dat.
  • Bude to trvat déle.

Proto omezte použití integračních testů na nejdůležitější scénáře infrastruktury. Pokud je možné testovat chování pomocí testu jednotek nebo integračního testu, zvolte test jednotek.

V diskusích o integračních testech se testovaný projekt často nazývá systém pod testem nebo "SUT". "SUT" se používá v tomto článku k odkazování na aplikaci ASP.NET Core, která se testuje.

Nezapisujte integrační testy pro každou permutaci přístupu k datům a souborům v databázích a souborových systémech. Bez ohledu na to, kolik míst v aplikaci interaguje s databázemi a souborovými systémy, obvykle stačí zaměřená sada testů integrace pro čtení, zápis, aktualizaci a odstranění k dostatečnému testování komponentů databáze a souborového systému. Pro rutinní testy logiky metody, které s těmito komponentami pracují, použijte testy jednotek. Při testování jednotek může použití falešných nebo napodobených infrastruktur vést k rychlejšímu provádění testů.

ASP.NET Core integrační testy

Integrační testy v ASP.NET Core vyžadují následující:

  • Projekt testů se používá k zahrnutí a provádění testů. Testovací projekt má odkaz na SUT.
  • Testovací projekt vytvoří testovacího webového hostitele pro SUT a použije klienta testovacího serveru ke zpracování požadavků a odpovědí pomocí SUT.
  • Spouštěč testů se používá k provádění testů a hlášení výsledků testu.

Integrační testy se řídí posloupností událostí, které zahrnují obvyklé testovací kroky Uspořádat, Jednat a Assert :

  1. Webový hostitel SUT je nakonfigurovaný.
  2. Vytvoří se testovací serverový klient pro odesílání požadavků do aplikace.
  3. Je spuštěn testovací krok Příprava: Testovací aplikace připraví požadavek.
  4. Testovací krok Act je spuštěn: Klient odešle požadavek a obdrží odpověď.
  5. Provede se testovací krok Assert: Skutečná odpověď se ověří jako úspěšná nebo neúspěšná podle očekávané odpovědi.
  6. Proces pokračuje, dokud nejsou všechny testy provedeny.
  7. Výsledky testu jsou hlášeny.

Obvykle je testovací webový hostitel nakonfigurovaný jinak než normální webový hostitel aplikace pro testovací běhy. Pro testy se například může použít jiná databáze nebo jiná nastavení aplikace.

Komponenty infrastruktury, jako je testovací webový hostitel a testovací server v paměti (TestServer), jsou poskytovány nebo spravovány balíčkem Microsoft.AspNetCore.Mvc.Testing . Použití tohoto balíčku zjednodušuje vytváření a spouštění testů.

Balíček Microsoft.AspNetCore.Mvc.Testing zpracovává následující úlohy:

  • Zkopíruje soubor závislostí (.deps) z SUT do adresáře testovacího bin projektu.
  • Nastaví kořen obsahu na kořen projektu SUT tak, aby při spuštění testů byly statické soubory a stránky/zobrazení dostupné.
  • Poskytuje třídu WebApplicationFactory pro zjednodušení inicializace SUT pomocí .

Dokumentace k testům jednotek popisuje, jak nastavit testovací projekt a spouštěč testů, spolu s podrobnými pokyny ke spouštění testů a doporučení pro pojmenování testů a tříd testů.

Oddělte testy jednotek od integračních testů do různých projektů. Oddělení testů:

  • Pomáhá zajistit, aby součásti testování infrastruktury nejsou omylem zahrnuty do testů jednotek.
  • Umožňuje kontrolu nad tím, jakou sadu testů se spouští.

Konfigurace pro testy Razor aplikací Pages a aplikací MVC není prakticky nijak rozdílná. Jediný rozdíl spočívá v tom, jak jsou testy pojmenovány. Razor V aplikaci Pages jsou testy koncových bodů stránky obvykle pojmenovány za třídou modelu stránky (například IndexPageTests pro testování integrace součástí pro indexovou stránku). V aplikaci MVC jsou testy obvykle uspořádány podle tříd kontroleru a pojmenovány podle kontrolerů, které testují (například HomeControllerTests pro testování integrace součástí pro Home kontroler).

Požadavky na testovací aplikaci

Testovací projekt musí:

Tyto požadavky se dají zobrazit v ukázkové aplikaci. tests/RazorPagesProject.Tests/RazorPagesProject.Tests.csproj Zkontrolujte soubor. Ukázková aplikace používá testovací architekturu xUnit a knihovnu analyzátoru AngleSharp , takže ukázková aplikace také odkazuje:

V aplikacích, které používají xunit.runner.visualstudio verzi 2.4.2 nebo novější, musí testovací projekt odkazovat na Microsoft.NET.Test.Sdk balíček.

Entity Framework Core se také používá v testech. Prohlédněte si soubor projektu na GitHubu.

Prostředí SUT

Pokud prostředí SUT není nastavené, prostředí se ve výchozím nastavení nastaví na Vývoj.

Základní testy s výchozím WebApplicationFactory

Implicitně definovanou Program třídu zpřístupňte testovacímu projektu jedním z následujících způsobů:

  • Zveřejnění interních typů z webové aplikace do testovacího projektu To lze provést v souboru projektu SUT (.csproj):

    <ItemGroup>
         <InternalsVisibleTo Include="MyTestProject" />
    </ItemGroup>
    
  • Program Nastavení třídy jako veřejné pomocí částečné deklarace třídy:

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

    Ukázková aplikace používá přístup částečné Program třídy.

WebApplicationFactory<TEntryPoint> slouží k vytvoření TestServer pro integrační testy. TEntryPoint je vstupní bod třídy SUT, obvykle Program.cs.

Třídy testů implementují rozhraní třídní fixatury () aby se označilo, že třída obsahuje testy, a poskytují sdílené instance objektů pro testy ve třídě.

Následující testovací třída BasicTests používá WebApplicationFactory ke spuštění SUT a poskytuje HttpClient testovací metodě Get_EndpointsReturnSuccessAndCorrectContentType. Metoda ověří, že stavový kód odpovědi je úspěšný (200–299) a hlavička Content-Type je text/html; charset=utf-8 pro několik stránek aplikace.

CreateClient() vytvoří instanci HttpClient , která automaticky sleduje přesměrování a zpracovává soubory cookie.

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

Ve výchozím nastavení se soubory cookie, které nejsou nezbytné, nezachovávají mezi požadavky, když jsou povoleny zásady souhlasu podle Obecného nařízení o ochraně osobních údajů (GDPR). Pokud chcete zachovat soubory cookie, které nejsou nezbytné, například soubory používané poskytovatelem TempData, označte je v testech jako nezbytné. Pokyny k označení cookie jako nezbytné naleznete v části Základní soubory cookie.

AngleSharp vs Application Parts pro antiforgery ověření

Tento článek používá analyzátor AngleSharp ke zpracování kontrol proti padělání tím, že načítá stránky a analyzuje HTML. Pokud chcete otestovat koncové body zobrazení kontroleru a Razor stránek na nižší úrovni, aniž byste se museli zabývat tím, jak se vykreslují v prohlížeči, zvažte použití Application Parts. Přístup Application Parts vloží kontroler nebo Razor stránku do aplikace, což lze použít k odesílání JSON požadavků pro získání potřebných hodnot. Další informace najdete v blogu o testování integrace prostředků ASP.NET Core chráněných antiforgery pomocí částí aplikace a přidruženém úložišti GitHub, od Martina Costella.

Přizpůsobení služby WebApplicationFactory

Konfiguraci webového hostitele lze vytvořit nezávisle na testovacích třídách tím, že zdědíte z WebApplicationFactory<TEntryPoint>, abyste vytvořili jednu nebo více vlastních továren:

  1. Dědit z WebApplicationFactory a přepsat ConfigureWebHost. Tento prvek IWebHostBuilder umožňuje konfiguraci kolekce služeb pomocí 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");
        }
    }
    

    Naplnění databáze v ukázkové aplikaci se provádí metodou InitializeDbForTests. Metoda je popsána v sekci Ukázka integračních testů: Organizace testovací aplikace.

    Kontext databáze SUT je registrován v Program.cs. Zpětné volání testovací aplikace builder.ConfigureServices se spustí po spuštění kódu aplikace Program.cs . Chcete-li použít jinou databázi pro testy, než je databáze aplikace, musí být kontext databáze aplikace nahrazen v builder.ConfigureServices.

    Ukázková aplikace najde popisovač služby pro kontext databáze a pomocí popisovače odebere registraci služby. Továrna pak přidá nový ApplicationDbContext, který využívá databázi v paměti pro testování.

    Pokud se chcete připojit k jiné databázi, změňte soubor DbConnection. Použití testovací databáze SQL Serveru:

  1. Použijte vlastní CustomWebApplicationFactory v testovacích třídách. Příklad níže používá továrnu ve třídě 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();
        }
    

    Klient ukázkové aplikace je nakonfigurovaný tak, aby zabránil HttpClient následujícím přesměrováním. Jak je vysvětleno později v části Napodobení ověřování , umožňuje testům zkontrolovat výsledek první odpovědi aplikace. První odpověď v mnoha těchto testech je přesměrování s hlavičkou Location.

  2. Typický test používá HttpClient metody a pomocné metody ke zpracování požadavku a odpovědi:

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

Všechny požadavky POST na SUT musí splňovat antiforgery kontrolu, která je automaticky provedena systémem ochrany dat aplikace. Aby bylo možné zajistit požadavek POST testu, musí testovací aplikace:

  1. Vytvořte žádost o stránku.
  2. Parsujte antiforgery token cookie a token pro ověření požadavku z odpovědi.
  3. Vytvořte žádost POST s použitím antiforgery cookie a ověřovacího tokenu žádosti.

Pomocné SendAsync rozšiřující metody (Helpers/HttpClientExtensions.cs) a GetDocumentAsync pomocná metoda (Helpers/HtmlHelpers.cs) v ukázkové aplikaci používají analyzátor AngleSharp ke zpracování kontroly antiforgery následujícími metodami:

  • GetDocumentAsync: Přijme HttpResponseMessage a vrátí hodnotu IHtmlDocument. GetDocumentAsync používá továrnu, která připraví virtuální odpověď na základě původní HttpResponseMessage. Další informace najdete v dokumentaci AngleSharp.
  • SendAsync metody rozšíření pro HttpClient vytvoření žádosti HttpRequestMessage a volání SendAsync(HttpRequestMessage) k odeslání žádostí do SUT. Přetížení pro SendAsync přijímají HTML formulář (IHtmlFormElement) a následující:
    • Tlačítko Odeslat formulář (IHtmlElement)
    • Kolekce hodnot formulářů (IEnumerable<KeyValuePair<string, string>>)
    • Tlačítko Odeslat (IHtmlElement) a hodnoty formuláře (IEnumerable<KeyValuePair<string, string>>)

AngleSharp je knihovna analýzy třetí strany používaná pro demonstrační účely v tomto článku a ukázkové aplikaci. AngleSharp se nepodporuje ani nevyžaduje pro testování integrace aplikací ASP.NET Core. Dalšími analyzátory, které lze použít, je například Html Agility Pack (HAP). Dalším přístupem je napsat kód pro zpracování ověřovacího tokenu, který systém antiforgery používá pro ověřování žádostí, a řídit antiforgery cookie přímo. Další informace najdete v části AngleSharp vs Application Parts kontroly antiforgery v tomto článku.

Poskytovatel databáze EF-Core v paměti lze použít pro omezené a základní testování, avšak pro testování v paměti se doporučuje poskytovatel SQLite.

Viz Rozšíření spuštění pomocí startovacích filtrů, který ukazuje, jak nakonfigurovat middleware pomocí IStartupFilter, což je užitečné, když test vyžaduje vlastní službu nebo middleware.

Přizpůsobení klienta pomocí withWebHostBuilder

Pokud je v rámci testovací metody vyžadována další konfigurace, WithWebHostBuilder vytvoří novou WebApplicationFactory s IWebHostBuilder, která je dále přizpůsobena pomocí konfigurace.

Ukázkový kód volá WithWebHostBuilder, aby nahradil nakonfigurované služby testovacími zástupci. Další informace a příklady použití najdete v části Vložení napodobených služeb v tomto článku.

Metoda Post_DeleteMessageHandler_ReturnsRedirectToRoot testu ukázkové aplikace ukazuje použití WithWebHostBuilder. Tento test provede odstranění záznamu v databázi aktivací odeslání formuláře v SUT.

Protože jiný test ve IndexPageTests třídě provádí operaci, která odstraní všechny záznamy v databázi a může běžet před Post_DeleteMessageHandler_ReturnsRedirectToRoot metodou, databáze bude znovu provedena v této testovací metodě, aby se zajistilo, že záznam existuje pro SUT k odstranění. Výběr prvního tlačítka messages pro odstranění formuláře v SUT se simuluje v požadavku na 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("/"));
}

Možnosti klienta

Podívejte se na WebApplicationFactoryClientOptions stránku s výchozími a dostupnými možnostmi při vytváření HttpClient instancí.

WebApplicationFactoryClientOptions Vytvořte třídu a předejte ji metodě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();
    }

POZNÁMKA: Pokud se chcete vyhnout upozorněním přesměrování HTTPS v protokolech při použití middlewaru přesměrování HTTPS, nastavte BaseAddress = new Uri("https://localhost")

Vložte simulované služby

Služby je možné přepsat v testu voláním ConfigureTestServices na builderu hostitele. K určení rozsahu přepsaných služeb na samotný test se používá metoda WithWebHostBuilder k načtení host builderu. To lze vidět v následujících testech:

Příklad SUT obsahuje vymezenou službu, která vrací citát. Citace je vložena do skrytého pole na stránce Index, když je stránka Index žádána.

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

Při spuštění aplikace SUT se vygeneruje následující kód:

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

K otestování injektáže služby a nabídky v integračním testu se do testu vloží napodobená služba. Služba napodobení nahradí aplikaci QuoteService službou poskytovanou testovací aplikací, která se nazývá 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 je volána a služba s vymezeným oborem je zaregistrovaná:

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

Kód vytvořený během provádění testu odráží uvozovkový text zadaný TestQuoteService, a proto kontrola projde:

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

Simulované ověřování

Testy ve AuthTests třídě kontrolují, že zabezpečený koncový bod:

  • Přesměruje neověřeného uživatele na přihlašovací stránku aplikace.
  • Vrátí obsah pro ověřeného uživatele.

V SUT stránka /SecurePage používá AuthorizePage konvenci pro aplikaci AuthorizeFilter na stránku. Další informace najdete v tématu Razor Konvence autorizace stránek.

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

V testu Get_SecurePageRedirectsAnUnauthenticatedUser je možnost WebApplicationFactoryClientOptions nastavena tak, aby zakázala přesměrování nastavením AllowAutoRedirect na 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"));
}

Když zakážete klientovi sledovat přesměrování, můžete provést následující kontroly:

  • Stavový kód vrácený SUT lze zkontrolovat podle očekávaného HttpStatusCode.Redirect výsledku, nikoli konečný stavový kód po přesměrování na přihlašovací stránku, což by bylo HttpStatusCode.OK.
  • Hodnota záhlaví Location v hlavičkách odpovědi se kontroluje, aby se ověřilo, že začíná na http://localhost/Identity/Account/Login, nikoli na konečné odpovědi přihlašovací stránky, kde by záhlaví Location nebylo k dispozici.

Testovací aplikace může simulovat AuthenticationHandler<TOptions> v ConfigureTestServices za účelem testování aspektů ověřování a autorizace. Minimální scénář vrátí 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);
    }
}

Když je schéma ověřování nastaveno na TestAuthHandler, volá se TestScheme k ověření uživatele, kde AddAuthentication je registrován pro ConfigureTestServices. Je důležité, TestScheme aby schéma odpovídalo schématu, které vaše aplikace očekává. Jinak ověřování nebude fungovat.

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

Další informace o WebApplicationFactoryClientOptionsnaleznete v části možnosti klienta.

Základní testy pro ověřovací middleware

Základní testy middlewaru ověřování najdete v tomto úložišti GitHubu. Obsahuje testovací server specifický pro testovací scénář.

Nastavení prostředí

Nastavte prostředí ve vlastní objektu pro vytváření aplikací:

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

Způsob, jakým testovací infrastruktura odvodí kořenovou cestu obsahu aplikace

Konstruktor WebApplicationFactory odvodí kořenovou cestu obsahu aplikace tak, že vyhledá WebApplicationFactoryContentRootAttribute v sestavení obsahující integrační testy s klíčem, který se rovná TEntryPoint sestavení System.Reflection.Assembly.FullName. Pokud se atribut se správným klíčem nenajde, WebApplicationFactory vrátí se zpět do hledání souboru řešení (.sln) a připojí TEntryPoint název sestavení do adresáře řešení. Kořenový adresář aplikace (kořenová cesta k obsahu) slouží ke zjišťování zobrazení a souborů obsahu.

Zakázat stínové kopírování

Stínová kopie způsobí, že testy se spustí v jiném adresáři než ve výstupním adresáři. Pokud vaše testy spoléhají na načítání souborů vzhledem k Assembly.Location a dochází k problémům, možná budete muset zakázat stínové kopírování.

Pokud chcete zakázat stínové kopírování při použití xUnit, vytvořte v adresáři testovacího xunit.runner.json projektu soubor se správným nastavením konfigurace:

{
  "shadowCopy": false
}

Likvidace objektů

Po provedení testů implementace IClassFixture jsou TestServer a HttpClient uvolněny, když xUnit uvolní WebApplicationFactory. Pokud instance objektů vytvořených vývojářem vyžadují odstranění, odstraňte je v implementaci IClassFixture. Další informace naleznete v tématu Implementace metody Dispose.

Po provedení testů TestClass jsou TestServer a HttpClient odstraněny, když MSTest odstraní WebApplicationFactory v metodě ClassCleanup. Pokud instance objektů vytvořených vývojářem vyžaduje odstranění, odstraňte je v ClassCleanup metodě. Další informace naleznete v tématu Implementace metody Dispose.

Po provedení testů třídy testu jsou TestServer a HttpClient uvolněny, když NUnit odstraní WebApplicationFactory v metodě TearDown. Pokud instance objektů vytvořených vývojářem vyžaduje odstranění, odstraňte je v TearDown metodě. Další informace naleznete v tématu Implementace metody Dispose.

Ukázka integračních testů

Ukázková aplikace se skládá ze dvou aplikací:

Aplikace Adresář projektu Popis
Aplikace zpráv (SUT) src/RazorPagesProject Umožňuje uživateli přidávat, odstraňovat, odstraňovat všechny a analyzovat zprávy.
Testovací aplikace tests/RazorPagesProject.Tests Používá se k provádění integračního testování SUT.

Testy je možné spouštět pomocí integrovaných testovacích funkcí IDE, například Visual Studio. Pokud používáte Visual Studio Code nebo příkazový řádek, spusťte v adresáři tests/RazorPagesProject.Tests následující příkaz:

dotnet test

Organizace aplikace zpráv (SUT)

SUT je Razor systém zpráv Pages s následujícími vlastnostmi:

  • Indexová stránka aplikace (Pages/Index.cshtml a Pages/Index.cshtml.cs) poskytuje uživatelské rozhraní a metody modelu stránky pro řízení sčítání, odstranění a analýzy zpráv (průměrná slova na zprávu).
  • Zpráva je popsána Message třídou (Data/Message.cs) se dvěma vlastnostmi: Id (klíč) a Text (zpráva). Vlastnost Text je povinná a omezená na 200 znaků.
  • Zprávy se ukládají pomocí databáze v paměti entity Framework†.
  • Aplikace obsahuje vrstvu přístupu k datům (DAL) ve své třídě AppDbContext kontextu databáze (Data/AppDbContext.cs).
  • Pokud je databáze při spuštění aplikace prázdná, úložiště zpráv se inicializuje se třemi zprávami.
  • Aplikace zahrnuje /SecurePage, ke kterému má přístup pouze ověřený uživatel.

Článek EF, Test s InMemory vysvětluje, jak používat in-memory databázi pro testování s MSTest. Toto téma používá testovací architekturu xUnit . Koncepty testů a implementace testů v různých testovacích architekturách jsou podobné, ale ne identické.

I když aplikace nepoužívá vzor úložiště a není efektivním příkladem Unit of Work (UoW) pattern, stránky podporují tyto vzory vývoje. Další informace najdete v tématu Návrh vrstvy trvalosti infrastruktury a logiky kontroleru testů (ukázka implementuje vzor úložiště).

Testovací organizace aplikací

Testovací aplikace je konzolová aplikace uvnitř tests/RazorPagesProject.Tests adresáře.

Testovací adresář aplikace Popis
AuthTests Obsahuje testovací metody pro:
  • Přístup k zabezpečené stránce neověřeným uživatelem
  • Přístup ověřeného uživatele k zabezpečené stránce s využitím simulace AuthenticationHandler<TOptions>.
  • Získání profilu uživatele GitHubu a kontrola přihlášení uživatele profilu
BasicTests Obsahuje testovací metodu pro směrování a typ obsahu.
IntegrationTests Obsahuje integrační testy stránky indexu pomocí vlastní WebApplicationFactory třídy.
Helpers/Utilities
  • Utilities.cs obsahuje metodu použitou InitializeDbForTests k vytvoření databáze s testovacími daty.
  • HtmlHelpers.cs poskytuje metodu, která vrací AngleSharp IHtmlDocument pro použití testovacími metodami.
  • HttpClientExtensions.cs poskytuje přetížení pro SendAsync k odesílání požadavků do SUT.

Testovací architektura je xUnit. Integrační testy se provádějí pomocí Microsoft.AspNetCore.TestHost, která zahrnuje TestServer. Vzhledem k tomu, že se Microsoft.AspNetCore.Mvc.Testing balíček používá ke konfiguraci testovacího hostitele a testovacího serveru, TestHost nevyžadují a TestServer balíčky přímé odkazy na balíčky v souboru projektu testovací aplikace nebo konfiguraci vývojáře v testovací aplikaci.

Integrační testy obvykle před spuštěním testu vyžadují malou datovou sadu v databázi. Například test mazání vyžaduje odstranění záznamu z databáze, takže je třeba, aby databáze měla alespoň jeden záznam, aby žádost o odstranění byla úspěšná.

Ukázková aplikace naplní databázi třemi zprávami v Utilities.cs, které testy mohou použít při spuštění:

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

Kontext databáze SUT je registrován v Program.cs. Zpětné volání testovací aplikace builder.ConfigureServices se spustí po spuštění kódu aplikace Program.cs . Chcete-li pro testy použít jinou databázi, musí být kontext databáze aplikace nahrazen v builder.ConfigureServices. Další informace najdete v části Přizpůsobení WebApplicationFactory.

Další materiály