Test ASP.NET Core aplikací MVC

"Pokud si nejste spokojeni s testováním částí vašeho produktu, pravděpodobně ho vaši zákazníci nechtějí testovat, buď." _Anonymous

Software jakékoli složitosti může při reakci na změny selhat neočekávaným způsobem. Proto se testování po provedení změn vyžaduje pro všechny, ale nejvíce triviální (nebo nejméně kritické) aplikace. Manuální testování je nejpomalejší, nejméně spolehlivý a nejnákladný způsob testování softwaru. Pokud aplikace není navržena tak, aby se testovatelné, může to být pouze to, co je k dispozici. Aplikace napsané pro sledování principů architektury stanovených v kapitole 4 by měly být jednotky testovatelné. ASP.NET Core aplikace podporují automatizovanou integraci a funkční testování.

Druhy automatizovaných testů

Existuje mnoho druhů automatizovaných testů pro softwarové aplikace. Nejjednodušším, nejnižším testem úrovně je test jednotky. Na poněkud vyšší úrovni jsou integrační testy a funkční testy. Jiné druhy testů, jako jsou například testy uživatelského rozhraní, zátěžové testy, zátěžové testy a testy kouře, jsou nad rámec tohoto dokumentu.

Testování částí

Test jednotek testuje jednu část logiky vaší aplikace. Jednu z nich může popsána uvedením některých věcí, které nejsou. Test jednotek netestuje, jak váš kód funguje se závislostmi nebo infrastrukturou – to je to, pro které jsou k disviset testy. Test jednotek netestuje rozhraní, ve kterém je váš kód napsán – měli byste předpokládat, že funguje, nebo pokud ho nenajdete, poznamenejte si chybu a kód a požádejte ho. Test jednotek běží zcela v paměti a zpracovává se. Nekomunikuje se systémem souborů, sítí nebo databází. Testy jednotek by měly testovat pouze váš kód.

Testování částí, na základě skutečnosti, že testuje pouze jednu jednotku vašeho kódu bez externích závislostí, by mělo být provedeno extrémně rychle. Proto byste měli být schopni spustit testovací sady stovek jednotek testů za několik sekund. Spouštějte je často, ideálně před každým vložením do sdíleného úložiště správy zdrojového kódu a jistě u každého automatizovaného sestavení na serveru sestavení.

Integrační testy

I když je vhodné zapouzdřit kód, který komunikuje s infrastrukturou, jako jsou databáze a systémy souborů, budete mít i nějaký kód a budete ho pravděpodobně chtít otestovat. Kromě toho byste měli ověřit, že vrstvy kódu pracují podle očekávání, když jsou závislosti vaší aplikace zcela vyřešeny. Tato funkce je odpovědná za testy Integration. Testy integrace mají za následek pomalejší a obtížnější nastavení než testování částí, protože jsou často závislé na externích závislostech a infrastruktuře. Proto byste se měli vyhnout testování věcí, které by mohly být testovány pomocí testů jednotek v rámci integračních testů. Pokud otestujete daný scénář s testováním částí, měli byste ho otestovat pomocí testu jednotek. Pokud nemůžete, zvažte použití integračního testu.

Integrační testy často mají složitější nastavení a rozboru postupy než testy jednotek. Například test integrace, který se dostane ke skutečné databázi, bude potřebovat způsob, jak vrátit databázi do známého stavu před každým spuštěním testu. Při přidání nových testů a vývoje produkčního schématu databáze budou tyto testovací skripty v úmyslu růst velikosti a složitosti. V mnoha velkých systémech je nepraktické spouštět úplné sady integračních testů na pracovních stanicích pro vývojáře před vrácením změn do správy sdíleného zdrojového kódu. V těchto případech mohou být integrační testy spuštěny na serveru sestavení.

Funkční testy

Integrační testy se napíší z perspektivy vývojářů, aby bylo možné ověřit, že některé součásti systému fungují správně společně. Funkční testy se zapisují z perspektivy uživatele a ověřují správnost systému na základě jeho požadavků. Následující úryvek nabízí užitečný analogový způsob, jak si představit o funkčních testech v porovnání s testováním částí:

"" Hodně vývoje systému likened do budování domu. I když tato analogová možnost není poměrně správná, můžeme ji pro účely porozumění rozdílu mezi jednotkou a funkčními testy roztáhnout. Testování částí je obdobné jako inspektor stavby, který se navštíví na staveništi. Zaměřuje se na různé interní systémy domu, základů, rámců, elektroinstalace, instalací a tak dále. Zajišťuje (testuje), že části domu budou fungovat správně a bezpečně, to znamená, že budou splňovat stavební kód. Funkční testy v tomto scénáři jsou obdobou domácnosti návštěvě tohoto téhož staveništového webu. Předpokládá, že se interní systémy budou chovat patřičně, takže inspektor budovy provádí jeho úlohu. Domácnosti se zaměřuje na to, co bude v této domácnosti v provozu. Záleží na tom, jak se na pracovišti nachází, na různých místnostech a na tom, kde se v rodině hodí, jsou Windows na dobrém místě pro zachycení ráno. Domácnosti provádí funkční testy na domu. Má perspektivu uživatele. Inspektor budovy provádí testování částí na pracovišti. Má perspektivu tvůrce. "

Zdroj: testování částí versus funkční testy

Fond jsem se říkám vývojářům, že nedošlo k chybě dvěma způsoby: nepovedlo se nám sestavit chybu, nebo jsme vytvořili špatné věci. " Testování částí vám zajistí, že vytváříte přímo věc. funkční testy zajistí, že vytváříte správnou věc.

Vzhledem k tomu, že funkční testy fungují na úrovni systému, mohou vyžadovat určitý stupeň automatizace uživatelského rozhraní. Stejně jako integrační testy obvykle pracují s určitým druhem testovací infrastruktury. Tato aktivita zajišťuje pomalejší a větší poměrně křehký než testy jednotek a integrace. Měli byste mít jenom tolik funkčních testů, protože potřebujete mít jistotu, že se systém chová, jako uživatelé očekávají.

Testovací jehlan

Martin Fowlera zapsaný k testovacímu pyramidu, který je příkladem zobrazeného na obrázku 9-1.

Testovací jehlan

Obrázek 9-1. Testovací jehlan

Různé vrstvy pyramidy a jejich relativní velikosti, reprezentují různé druhy testů a počet, kolik byste měli pro svou aplikaci psát. Jak vidíte, doporučuje se mít velký základ testování částí, který je podporován menší vrstvou integračních testů, s ještě menší vrstvou funkčních testů. Každá vrstva by v ideálním případě měla mít pouze testy, které nemohou být provedeny přiměřeně na nižší vrstvě. Mějte na paměti, že při pokusu o určení toho, jaký typ testu potřebujete pro konkrétní scénář, mějte na paměti jehlany s testováním.

Co testovat

Běžný problém pro vývojáře, kteří mají zkušenosti s psaním automatizovaných testů, se blíží k testování. Dobrým výchozím bodem je testování podmíněné logiky. Kdekoli máte metodu s chováním, které se mění v závislosti na podmíněném příkazu (Pokud-else, přepínač a tak dále), měli byste být schopni se připojit alespoň k několika testům, které pro určité podmínky potvrzují správné chování. Pokud má váš kód chybové podmínky, je vhodné napsat alespoň jeden test pro "příjemné" cestu prostřednictvím kódu (bez chyb) a alespoň jeden test pro "cestu JSD" (s chybami nebo netypickými výsledky) k potvrzení, že se aplikace chová jako očekávaná na výskytu chyb. Nakonec se snažte soustředit na testování věcí, které mohou selhat, a nemusíte se zaměřit na metriky, jako je pokrytí kódu. Větší pokrytí kódu je lepší než méně, obecně. Nicméně psaní několika dalších testů složitých a podnikových metod je obvykle vhodnější než při psaní testů pro automatické vlastnosti, a to jenom pro zlepšení metrik pokrytí testovacího kódu.

Uspořádání projektů testů

Projekty testů mohou být uspořádány, ale budou pro vás nejlépe fungovat. Je vhodné oddělit testy podle typu (testování částí, test integrace) a podle toho, co jsou testovány (podle projektu, podle oboru názvů). Bez ohledu na to, zda se toto oddělení skládá ze složek v rámci jednoho testovacího projektu nebo více testovacích projektů, je rozhodnutí o návrhu. Jeden projekt je nejjednodušší, ale pro velké projekty s mnoha testy nebo pro snazší spouštění různých sad testů může být vhodné mít několik různých testovacích projektů. Mnoho týmů organizuje projekty testů na základě projektu, který testuje, což pro aplikace s více než několika projekty může vést k velkému počtu testovacích projektů, zejména v případě, že je stále rozdělíte podle toho, jaké typy testů jsou v jednotlivých projektech. Přístup k ohrožení je mít jeden projekt na typ testu, na aplikaci a složky uvnitř testovacích projektů k označení projektu (a třídy), který je testován.

Běžným přístupem je uspořádání projektů aplikace do složky src a testovacích projektů aplikace v rámci paralelní "testy". pokud tuto organizaci najdete jako užitečnou, můžete vytvořit vyhovující složky řešení v Visual Studio.

Testování organizace ve vašem řešení

Obrázek 9-2. Testování organizace ve vašem řešení

Můžete použít jakékoli testovací rozhraní, které dáváte přednost. xUnit framework dobře funguje a je to, co všechny ASP.NET Core a EF Core testy jsou napsány v. projekt testů xUnit můžete přidat do Visual Studio pomocí šablony zobrazené na obrázku 9-3 nebo z CLI pomocí dotnet new xunit .

přidat Project testů xUnit do Visual Studio

Obrázek 9-3. přidat Project testů xUnit do Visual Studio

Pojmenování testů

Testy pojmechujte konzistentně s názvy, které označují, co každý test dělá. Jedním z přístupů, se které se mi posměšně postoupily, je název testovacích tříd podle třídy a metody, které testují. Výsledkem tohoto přístupu je mnoho malých testovacích tříd, ale je velmi jasné, za co jsou jednotlivé testy zodpovědné. Při nastavení názvu testovací třídy je možné k identifikaci třídy a metody, která se má testovat, použít název testovací metody k určení testovaného chování. Tento název by měl obsahovat očekávané chování a všechny vstupy nebo předpoklady, které by toto chování měly přinést. Několik příkladů názvů testů:

  • CatalogControllerGetImage.CallsImageServiceWithId

  • CatalogControllerGetImage.LogsWarningGivenImageMissingException

  • CatalogControllerGetImage.ReturnsFileResultWithBytesGivenSuccess

  • CatalogControllerGetImage.ReturnsNotFoundResultGivenImageMissingException

Varianta tohoto přístupu končí názvem každé testovací třídy na "Should" (Měl by) a mírně upravuje čas:

  • CatalogControllerGetImageMěl by . VoláníImageServiceWithId

  • CatalogControllerGetImageMěl by . ProtokolWarningGivenImageMissingException

Pro některé týmy je druhý přístup k pojmenování jasnější, i když trochu podrobnější. V každém případě se pokuste použít zásady vytváření názvů, které poskytují přehled o chování testů, takže když jeden nebo více testů selže, je z názvů těchto případů zřejmé, které případy selhaly. Vyhněte se pojmenování testů vágně, například ControllerTests.Test1, protože tyto názvy nemají žádnou hodnotu, když je uvidíte ve výsledcích testů.

Pokud dodržujete zásady vytváření názvů, jako je ta výše uvedená, která vytváří mnoho malých testovacích tříd, je vhodné dále uspořádat testy pomocí složek a oborů názvů. Obrázek 9–4 znázorňuje jeden přístup k uspořádání testů podle složky v několika testovacích projektech.

Uspořádání testovacích tříd podle složek na základě testované třídy

Obrázek 9–4. Uspořádání testovacích tříd podle složky na základě testované třídy

Pokud má konkrétní třída aplikace mnoho testovaných metod (a tedy mnoho testovacích tříd), může mít smysl umístit tyto třídy do složky odpovídající třídě aplikace. Tato organizace se neliší od způsobu uspořádání souborů do složek jinde. Pokud máte ve složce obsahující mnoho dalších souborů více než tři nebo čtyři související soubory, je často užitečné je přesunout do vlastní podsložky.

Testování částí ASP.NET Core aplikace

V dobře navržené ASP.NET Core aplikace bude většina složitosti a obchodní logiky zapouzdřena do obchodních entit a různých služeb. Samotný ASP.NET Core MVC s kontrolery, filtry, modely zobrazení a zobrazeními by měl vyžadovat několik testů jednotek. Velká část funkčnosti dané akce leží mimo samotnou metodu akce. Testování, jestli směrování nebo globální zpracování chyb funguje správně, není možné efektivně provést pomocí testu jednotek. Podobně žádné filtry, včetně ověřování modelu a ověřovacích a autorizačních filtrů, nelze testovat pomocí testu, který cílí na metodu akce kontroleru. Bez těchto zdrojů chování by měla být většina metod akcí triviálně malá a delegovat většinu své práce na služby, které je možné testovat nezávisle na kontroleru, který je používá.

Někdy budete muset kód refaktorovat, abyste ho mohli testovat podle jednotek. Tato aktivita často zahrnuje identifikaci abstrakcí a použití injektáže závislostí pro přístup k abstrakci v kódu, který chcete testovat, a nekódování přímo proti infrastruktuře. Zvažte například tuto metodu snadné akce pro zobrazení obrázků:

[HttpGet("[controller]/pic/{id}")]
public IActionResult GetImage(int id)
{
    var contentRoot = _env.ContentRootPath + "//Pics";
    var path = Path.Combine(contentRoot, id + ".png");
    Byte[] b = System.IO.File.ReadAllBytes(path);
    return File(b, "image/png");
}

Testování částí této metody je obtížné díky přímé závislosti na metodě , kterou používá System.IO.File ke čtení ze systému souborů. Toto chování můžete otestovat, abyste zajistili, že funguje podle očekávání, ale u skutečných souborů je integrační test. Je vhodné poznamenat, že nemůžete testovat trasu této metody. Za chvíli uvidíte, jak to provést — s funkčním testem.

Pokud nemůžete přímo testovat chování systému souborů a nemůžete otestovat trasu, co je k otestování? Po refaktoringu, aby bylo testování jednotek možné, můžete zjistit některé testovací případy a chybějící chování, například zpracování chyb. Co metoda dělá, když se soubor nenašel? Co by měl udělat? V tomto příkladu vypadá refaktorovaná metoda následujícím způsobem:

[HttpGet("[controller]/pic/{id}")]
public IActionResult GetImage(int id)
{
    byte[] imageBytes;
    try
    {
        imageBytes = _imageService.GetImageBytesById(id);
    }
    catch (CatalogImageMissingException ex)
    {
        _logger.LogWarning($"No image found for id: {id}");
        return NotFound();
    }
    return File(imageBytes, "image/png");
}

_logger a _imageService se vloženého jako závislosti. Teď můžete otestovat, že stejné ID, které je předáno metodě akce, je předáno metodě a že výsledné bajty jsou vráceny jako součást _imageService FileResult. Můžete také otestovat, že protokolování chyb probíhá podle očekávání a že pokud image chybí, vrátí se výsledek za předpokladu, že toto chování je důležité chování aplikace (to znamená ne jenom dočasný kód, který vývojář přidal pro diagnostiku NotFound problému). Skutečná logika souborů se přesunula do samostatné implementační služby a byla rozšířena tak, aby v případě chybějícího souboru vracela výjimku specifickou pro aplikaci. Tuto implementaci můžete otestovat nezávisle pomocí integračního testu.

Ve většině případů budete chtít používat globální obslužné rutiny výjimek v kontrolerů, takže množství logiky v nich by mělo být minimální a pravděpodobně by za testování částí nemělo být vhodné. Většinu akcí kontroleru můžete testovat pomocí funkčních testů a TestServer třídy popsané níže.

Testování integrace ASP.NET Core aplikace

Většina integračních testů ve vašem projektu ASP.NET Core by měla být testovací služby a další typy implementace definované ve vašem projektu infrastruktury. Můžete například otestovat, jestli EF Core úspěšně aktualizována a načítá data, která očekáváte od tříd přístupu k datům, které se nachází v projektu Infrastruktura. Nejlepší způsob, jak otestovat, že se váš ASP.NET Core MVC chová správně, je použít funkční testy, které se spustí pro vaši aplikaci spuštěnou v testovacím hostiteli.

Funkční testování ASP.NET Core aplikací

Pro ASP.NET Core aplikace třída usnadňuje psaní TestServer funkčních testů. Můžete nakonfigurovat pomocí (nebo ) přímo (jako obvykle pro aplikaci), nebo s TestServer WebHostBuilder typem (k dispozici od verze HostBuilder WebApplicationFactory 2.1). Pokuste se co nejvíce spárovat testovacího hostitele s produkčním hostitelem, aby vaše testy procvičují chování podobné tomu, co bude aplikace dělat v produkčním prostředí. Třída WebApplicationFactory je užitečná při konfiguraci ContentRoot testového serveru, který používá ASP.NET Core k vyhledání statického prostředku, jako jsou zobrazení.

Jednoduché funkční testy můžete vytvořit vytvořením testovací třídy, která implementuje , kde je třída IClassFixture\<WebApplicationFactory\<TEntry>> TEntry vaší webové Startup aplikace. Díky tomuto rozhraní může testovací prostředí vytvořit klienta pomocí metody CreateClient továrny:

public class BasicWebTests : IClassFixture<WebApplicationFactory<Startup>>
{
    protected readonly HttpClient _client;

    public BasicWebTests(WebApplicationFactory<Startup> factory)
    {
        _client = factory.CreateClient();
    }

    // write tests that use _client
}

Před každým testovacím během budete často chtít provést nějakou další konfiguraci webu, například nakonfigurovat aplikaci tak, aby v paměti ukládala data a potom ji dosázla testovacími daty. K dosažení této funkce vytvořte vlastní podtřídu třídy a WebApplicationFactory\<TEntry> přepište její ConfigureWebHost metodu. Následující příklad pochází z projektu eShopOnWeb FunctionalTests a používá se jako součást testů hlavní webové aplikace.

using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.EntityFrameworkCore;
using Microsoft.eShopWeb.Infrastructure.Data;
using Microsoft.eShopWeb.Infrastructure.Identity;
using Microsoft.eShopWeb.Web;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;

namespace Microsoft.eShopWeb.FunctionalTests.Web
{
    public class WebTestFixture : WebApplicationFactory<Startup>
    {
        protected override void ConfigureWebHost(IWebHostBuilder builder)
        {
            builder.UseEnvironment("Testing");

            builder.ConfigureServices(services =>
            {
                 services.AddEntityFrameworkInMemoryDatabase();

                // Create a new service provider.
                var provider = services
                    .AddEntityFrameworkInMemoryDatabase()
                    .BuildServiceProvider();

                // Add a database context (ApplicationDbContext) using an in-memory
                // database for testing.
                services.AddDbContext<CatalogContext>(options =>
                {
                    options.UseInMemoryDatabase("InMemoryDbForTesting");
                    options.UseInternalServiceProvider(provider);
                });

                services.AddDbContext<AppIdentityDbContext>(options =>
                {
                    options.UseInMemoryDatabase("Identity");
                    options.UseInternalServiceProvider(provider);
                });

                // Build the service provider.
                var sp = services.BuildServiceProvider();

                // Create a scope to obtain a reference to the database
                // context (ApplicationDbContext).
                using (var scope = sp.CreateScope())
                {
                    var scopedServices = scope.ServiceProvider;
                    var db = scopedServices.GetRequiredService<CatalogContext>();
                    var loggerFactory = scopedServices.GetRequiredService<ILoggerFactory>();

                    var logger = scopedServices
                        .GetRequiredService<ILogger<WebTestFixture>>();

                    // Ensure the database is created.
                    db.Database.EnsureCreated();

                    try
                    {
                        // Seed the database with test data.
                        CatalogContextSeed.SeedAsync(db, loggerFactory).Wait();

                        // seed sample user data
                        var userManager = scopedServices.GetRequiredService<UserManager<ApplicationUser>>();
                        var roleManager = scopedServices.GetRequiredService<RoleManager<IdentityRole>>();
                        AppIdentityDbContextSeed.SeedAsync(userManager, roleManager).Wait();
                    }
                    catch (Exception ex)
                    {
                        logger.LogError(ex, $"An error occurred seeding the " +
                            "database with test messages. Error: {ex.Message}");
                    }
                }
            });
        }
    }
}

Testy mohou tuto vlastní instanci WebApplicationFactory využít k vytvoření klienta a pak k provádění požadavků na aplikaci pomocí této klientské instance. Aplikace bude mít data, která lze použít jako součást testovacích výrazů. Následující test ověří, že se domovská stránka aplikace eShopOnWeb načte správně a obsahuje výpis produktu, který byl do aplikace přidán jako součást předimových dat.

using Microsoft.eShopWeb.FunctionalTests.Web;
using System.Net.Http;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.eShopWeb.FunctionalTests.WebRazorPages
{
    [Collection("Sequential")]
    public class HomePageOnGet : IClassFixture<WebTestFixture>
    {
        public HomePageOnGet(WebTestFixture factory)
        {
            Client = factory.CreateClient();
        }

        public HttpClient Client { get; }

        [Fact]
        public async Task ReturnsHomePageWithProductListing()
        {
            // Arrange & Act
            var response = await Client.GetAsync("/");
            response.EnsureSuccessStatusCode();
            var stringResponse = await response.Content.ReadAsStringAsync();

            // Assert
            Assert.Contains(".NET Bot Black Sweatshirt", stringResponse);
        }
    }
}

Tento funkční test procvičuje kompletní ASP.NET Core MVC/ Razor Pages aplikací, včetně veškerého middlewaru, filtrů a vaacích, které mohou být na místě. Ověří, že given route ("/") vrátí očekávaný stavový kód úspěchu a výstup HTML. Dělá to tak, že nenastaví skutečný webový server, a vyhne se tak velké štěrosti, kterou může mít používání skutečného webového serveru pro testování (například problémy s nastavením brány firewall). Funkční testy, které běží na testserveru, jsou obvykle pomalejší než testy integrace a jednotek, ale jsou mnohem rychlejší než testy, které by se spouštěl přes síť na testovacím webovém serveru. Pomocí funkčních testů se ujistěte, že front-endový zásobník vaší aplikace funguje podle očekávání. Tyto testy jsou zvláště užitečné, když zjistíte duplikaci v kontrolerů nebo stránkách a řešíte duplikaci přidáním filtrů. V ideálním případě tento refaktoring nezmění chování aplikace a sada funkčních testů to ověří.

Reference – testovací ASP.NET Core MVC