Sdílet prostřednictvím


Testování aplikací ASP.NET Core MVC

Tip

Tento obsah je výňatek z eBooku, architekta moderních webových aplikací s ASP.NET Core a Azure, který je k dispozici na webu .NET Docs nebo jako bezplatný soubor PDF ke stažení, který si můžete přečíst offline.

Architect Modern Web Applications with ASP.NET Core and Azure eBook cover thumbnail.

"Pokud se vám nelíbí testování jednotek vašeho produktu, s největší pravděpodobností se vám to nebude líbit ani zákazníkům." _-Anonymní-

Software jakékoli složitosti může selhat neočekávaně v reakci na změny. Testování po provedení změn je tedy vyžadováno pro všechny, ale pro ty nejvýraznější (nebo nejméně kritické) aplikace. Ruční testování je nejpomalejší, nejméně spolehlivý a nejnákladnější způsob testování softwaru. Bohužel, pokud nejsou aplikace navrženy tak, aby byly testovatelné, může to být jediný dostupný způsob testování. Aplikace napsané tak, aby dodržovaly architektonické principy uvedené v kapitole 4 , by měly být z velké části testovatelné. ASP.NET základní aplikace podporují automatizovanou integraci a funkční testování.

Druhy automatizovaných testů

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

Testy jednotky

Test jednotek testuje jednu část logiky aplikace. Můžete ho dále popsat výpisem některých věcí, které nejsou. Test jednotek neotestuje, jak váš kód funguje se závislostmi nebo infrastrukturou – k čemu slouží integrační testy. Test jednotek neotestuje architekturu, na které je váš kód napsaný – měli byste předpokládat, že funguje, nebo pokud zjistíte, že nefunguje, vytvořte chybu a alternativní řešení kódu. Test jednotek běží zcela v paměti a v procesu. Nekomunikuje se systémem souborů, sítí ani databází. Testy jednotek by měly testovat pouze váš kód.

Testy jednotek by na základě skutečnosti, že testují pouze jednu jednotku kódu, bez externích závislostí, by se měla spouštět extrémně rychle. Proto byste měli být schopni spustit testovací sady stovek testů jednotek za několik sekund. Spouštějte je často, ideálně před každým nasdílením změn do sdíleného úložiště správy zdrojového kódu a určitě s každým automatizovaným sestavením na vašem buildovém serveru.

Integrační testy

I když je vhodné zapouzdření kódu, který komunikuje s infrastrukturou, jako jsou databáze a systémy souborů, stále budete mít nějaký z tohoto kódu a pravděpodobně ho budete chtít otestovat. Kromě toho byste měli ověřit, že vrstvy kódu komunikují podle očekávání, když jsou závislosti vaší aplikace plně vyřešené. Tato funkce je zodpovědností za integrační testy. Integrační testy mají tendenci být pomalejší a obtížnější nastavit než testy jednotek, protože často závisejí na externích závislostech a infrastruktuře. Proto byste se měli vyhnout testování, které by bylo možné testovat pomocí testů jednotek v integračních testech. Pokud můžete otestovat daný scénář pomocí testu jednotek, měli byste ho otestovat pomocí testu jednotek. Pokud to nemůžete, zvažte použití integračního testu.

Integrační testy mají často složitější postupy nastavení a odbourání než testy jednotek. Například integrační test, který jde proti skutečné databázi, bude potřebovat způsob, jak vrátit databázi do známého stavu před každým testovacím spuštěním. Při přidání nových testů a vývoji schématu produkční databáze se tyto testovací skripty obvykle zvětšují ve velikosti a složitosti. V mnoha rozsáhlých systémech je nepraktické spouštět úplné sady integračních testů na vývojářských pracovních stanicích před kontrolou změn ve sdílené správě zdrojového kódu. V těchto případech můžou být integrační testy spuštěny na buildovém serveru.

Funkční testy

Integrační testy se zapisují z pohledu vývojáře a ověřují, že některé součásti systému správně fungují. Funkční testy se zapisují z pohledu uživatele a na základě svých požadavků ověřují správnost systému. Následující výňatek nabízí užitečnou analogii, jak přemýšlet o funkčních testech v porovnání s testy jednotek:

"Často se vývoj systému podobá budově domu. I když tato analogie není úplně správná, můžeme ji rozšířit pro účely pochopení rozdílu mezi jednotkovými a funkčními testy. Testování jednotek je analogické s inspektorem budovy při návštěvě stavebního areálu domu. Zaměřuje se na různé vnitřní systémy domu, základ, rámování, elektrické, instalatérské a tak dále. Zajišťuje (testy), že části domu budou fungovat správně a bezpečně, to znamená, že splňují stavební kód. Funkční testy v tomto scénáři jsou analogické s majitelem domu, který navštíví stejnou stavební lokalitu. Předpokládá, že se vnitřní systémy budou chovat správně, že inspektor budovy provádí svůj úkol. Majitel domu se zaměřuje na to, co bude vypadat jako žít v tomto domě. Zajímá se o to, jak dům vypadá, jsou různé místnosti pohodlné velikosti, že dům odpovídá potřebám rodiny, jsou okna v dobrém místě, kde se chytí ranní slunce. Majitel domu provádí funkční testy na domě. Má perspektivu uživatele. Inspektor budovy provádí testy jednotek na domě. Má perspektivu tvůrce."

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

Rád říkám"Jako vývojáři selháváme dvěma způsoby: vytváříme špatnou věc, nebo vytváříme špatnou věc." Testy jednotek zajišťují, že vytváříte věc správně; funkční testy zajišťují, že vytváříte správnou věc.

Vzhledem k tomu, že funkční testy fungují na úrovni systému, mohou vyžadovat určitou míru automatizace uživatelského rozhraní. Podobně jako integrační testy obvykle pracují i s určitou testovací infrastrukturou. Díky této aktivitě jsou pomalejší a větší než testy jednotek a integrace. Měli byste mít jenom tolik funkčních testů, kolik potřebujete mít jistotu, že systém funguje podle očekávání uživatelů.

Testovací jehlan

Martin Fowler napsal o testovací pyramidě, příklad, který je znázorněn na obrázku 9-1.

Testing Pyramid

Obrázek 9–1 Testovací jehlan

Různé vrstvy jehlanu a jejich relativní velikosti představují různé druhy testů a kolik byste měli pro svou aplikaci napsat. Jak vidíte, doporučujeme mít velkou základnu testů jednotek podporovaných menší vrstvou integračních testů s ještě menší vrstvou funkčních testů. Každá vrstva by měla mít v ideálním případě pouze testy, které nelze provést dostatečně v nižší vrstvě. Mějte na paměti pyramidu testování, když se pokoušíte rozhodnout, jaký druh testu potřebujete pro konkrétní scénář.

Co otestovat

Běžným problémem pro vývojáře, kteří jsou nezkušeni psaním automatizovaných testů, je to, co testovat. 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í na základě podmíněného příkazu (if-else, switch atd.), měli byste být schopni přijít s alespoň několika testy, které potvrdí správné chování pro určité podmínky. Pokud váš kód obsahuje chybové stavy, je dobré napsat alespoň jeden test "šťastné cesty" prostřednictvím kódu (bez chyb) a alespoň jeden test pro "smutnou cestu" (s chybami nebo netypickými výsledky), abyste potvrdili, že se vaše aplikace chová podle očekávání v případě chyb. Nakonec se pokuste zaměřit na testování, které může selhat, a ne na metriky, jako je pokrytí kódu. Větší pokrytí kódu je lepší než méně, obecně. Zápis několika dalších testů komplexní a důležité metody pro firmu je ale obvykle vhodnější než psaní testů pro automatické vlastnosti pouze ke zlepšení metrik pokrytí kódu testu.

Uspořádání testovacích projektů

Projekty testů ale můžete uspořádat nejlépe za vás. Je vhodné oddělit testy podle typu (test jednotek, integrační test) a podle toho, co testují (podle projektu, podle oboru názvů). Ať už 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 snadnější spouštění různých sad testů můžete chtít mít několik různých testovacích projektů. Mnoho týmů uspořádá 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 pokud je stále rozdělíte podle toho, jaký druh testů jsou v každém projektu. Přístup k ohrožení zabezpečení spočívá v tom, že pro každou aplikaci bude jeden projekt na druh testu, ve složkách uvnitř testovacích projektů, které označují testovaný projekt (a třídu).

Běžným přístupem je uspořádání projektů aplikací ve složce src a testovacích projektů aplikace v rámci paralelní složky Tests. Pokud zjistíte, že tato organizace je užitečná, můžete vytvořit odpovídající složky řešení v sadě Visual Studio.

Test organization in your solution

Obrázek 9–2 Testování organizace v řešení

Podle toho, jakou testovací architekturu dáváte přednost, můžete použít. Architektura xUnit funguje dobře a je to, co všechny testy ASP.NET Core a EF Core jsou napsané. Testovací projekt xUnit v sadě Visual Studio můžete přidat pomocí šablony znázorněné na obrázku 9-3 nebo z rozhraní příkazového řádku pomocí dotnet new xunit.

Add an xUnit Test Project in Visual Studio

Obrázek 9–3 Přidání testovacího projektu xUnit v sadě Visual Studio

Pojmenování testů

Pojmenujte testy konzistentním způsobem s názvy, které označují, co každý test dělá. Jedním z přístupů, se kterými jsem měl velký úspěch, je pojmenovat třídy testů podle třídy a metody, které testují. Výsledkem tohoto přístupu je mnoho malých tříd testů, ale velmi jasně vyjasňuje, k čemu každý test odpovídá. Při nastavení názvu testovací třídy k identifikaci třídy a metody, která se má testovat, lze název testovací metody použít k určení chování, které se testuje. 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. Příklady názvů testů:

  • CatalogControllerGetImage.CallsImageServiceWithId

  • CatalogControllerGetImage.LogsWarningGivenImageMissingException

  • CatalogControllerGetImage.ReturnsFileResultWithBytesGivenSuccess

  • CatalogControllerGetImage.ReturnsNotFoundResultGivenImageMissingException

Varianta tohoto přístupu končí názvy každé testovací třídy na "Should" a mírně upraví čas:

  • CatalogControllerGetImageMěl by.zavolatImageServiceWithId

  • CatalogControllerGetImageMělo by.se protokolovat.WarningGivenImageMissingException

Některé týmy najdou druhý přístup k pojmenování jasnější, i když trochu více podrobné. V každém případě zkuste použít konvenci pojmenování, která poskytuje přehled o chování testu, takže když jeden nebo více testů selže, je zřejmé z jejich názvů, jaké případy selhaly. Vyhněte se vágním pojmenování testů, například ControllerTests.Test1, protože tyto názvy nenabízejí žádnou hodnotu, když je uvidíte ve výsledcích testu.

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

Organizing test classes by folder based on class being tested

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

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

Testování jednotek ASP.NET základních aplikací

V dobře navržené aplikaci ASP.NET Core se většina složitosti a obchodní logiky zapouzdřují do obchodních entit a různých služeb. Samotná aplikace ASP.NET Core MVC se svými kontrolery, filtry, modely zobrazení a zobrazeními by měla vyžadovat několik testů jednotek. Většina funkcí dané akce leží mimo samotnou metodu akce. Testování, zda směrování nebo globální zpracování chyb funguje správně, nelze efektivně provádět pomocí testu jednotek. Stejně tak všechny filtry, včetně ověření 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 většina metod akcí měla být triviálně malá a delegovat hromadnou práci na služby, které je možné testovat nezávisle na kontroleru, který je používá.

Někdy budete muset refaktorovat kód, abyste ho mohli testovat. 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 ne kódování přímo proti infrastruktuře. Představte si například tuto jednoduchou metodu 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í jednotek je obtížné díky přímé závislosti , na System.IO.Filekteré se používá ke čtení ze systému souborů. Toto chování můžete otestovat, abyste měli jistotu, že funguje podle očekávání, ale když to uděláte se skutečnými soubory, jedná se o integrační test. Za zmínku stojí, že nemůžete testovat trasu této metody jednotek – brzy se dozvíte, jak to udělat s funkčním testem.

Pokud nemůžete testovat chování systému souborů přímo a nemůžete otestovat trasu, co je potřeba testovat? No, po refaktoringu, aby bylo testování jednotek možné, můžete zjistit některé testovací případy a chybějící chování, jako je zpracování chyb. Co metoda dělá, když se soubor nenajde? Co by to mělo dělat? V tomto příkladu refaktorovaná metoda vypadá takto:

[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 oba jsou vloženy jako závislosti. Nyní můžete otestovat, že stejné ID, které je předáno _imageServicemetodě akce , a že výsledné bajty jsou vráceny jako součást FileResult. Můžete také otestovat, že protokolování chyb probíhá podle očekávání a že NotFound se vrátí výsledek, pokud obrázek chybí, za předpokladu, že toto chování je důležité chování aplikace (to znamená nejen dočasný kód, který vývojář přidal k diagnostice problému). Skutečná logika souboru se přesunula do samostatné implementační služby a byla rozšířena tak, aby vrátila výjimku specifickou pro aplikaci pro případ chybějícího souboru. Tuto implementaci můžete testovat nezávisle pomocí integračního testu.

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

Testování integrace ASP.NET základních aplikací

Většina integračních testů ve vašich aplikacích ASP.NET Core by měla testovat služby a další typy implementace definované v projektu Infrastruktura. Mohli byste například otestovat, že EF Core úspěšně aktualizovala a načítala data, která očekáváte z tříd přístupu k datům umístěných v projektu Infrastruktura. Nejlepším způsobem, jak otestovat, že se váš projekt ASP.NET Core MVC chová správně, je funkční testy, které se spouštějí v aplikaci spuštěné v testovacím hostiteli.

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

Pro aplikace ASP.NET Core TestServer třída usnadňuje zápis funkčních testů. Konfigurujete TestServer použití přímo WebHostBuilder (nebo HostBuilder) (jako obvykle pro vaši aplikaci) nebo s WebApplicationFactory typem (dostupným od verze 2.1). Snažte se co nejblíže spárovat svého testovacího hostitele s produkčním hostitelem, takže vaše testy se budou chovat podobně jako aplikace v produkčním prostředí. Třída WebApplicationFactory je užitečná pro konfiguraci ContentRoot serveru TestServer, 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 IClassFixture<WebApplicationFactory<TEntryPoint>>, kde TEntryPoint je třída vaší webové aplikace Startup . S tímto rozhraním může vaše testovací zařízení vytvořit klienta pomocí metody továrny CreateClient :

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

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

  // write tests that use _client
}

Tip

Pokud ve svém souboru Program.cs používáte minimální konfiguraci rozhraní API, třída bude ve výchozím nastavení deklarována interně a nebude přístupná z testovacího projektu. Místo toho můžete zvolit jakoukoli jinou třídu instance ve webovém projektu nebo ji přidat do souboru Program.cs :

// Make the implicit Program class public so test projects can access it
public partial class Program { }

Často budete chtít před každým testovacím spuštěním provést určitou další konfiguraci lokality, například nakonfigurovat aplikaci tak, aby používala úložiště dat v paměti a pak aplikaci osílala testovacími daty. Pokud chcete tuto funkci dosáhnout, vytvořte vlastní podtřídu WebApplicationFactory<TEntryPoint> a přepište její ConfigureWebHost metodu. Následující příklad je 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 můžou tuto vlastní aplikaci WebApplicationFactory použít k vytvoření klienta a následnému provádění požadavků na aplikaci pomocí této instance klienta. Aplikace bude obsahovat počáteční data, která lze použít jako součást kontrolních výrazů testu. Následující test ověří, že se domovská stránka aplikace eShopOnWeb správně načte a obsahuje výpis produktu, který byl přidán do aplikace jako součást počáteční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 provede kompletní ASP.NET zásobníku aplikací Core MVC / Razor Pages, včetně všech middlewarů, filtrů a pořadačů, které mohou být zavedeny. Ověří, že daná trasa ("/") vrací očekávaný stavový kód úspěchu a výstup HTML. Dělá to bez nastavení skutečného webového serveru a zabraňuje tomu, aby se při testování používal skutečný webový server (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 běžely přes síť na testovací webový server. 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ášť užitečné, když najdete duplicity na řadičích nebo stránkách a duplicitu řešíte přidáním filtrů. V ideálním případě toto refaktoring nezmění chování aplikace a sada funkčních testů ověří, že se jedná o tento případ.

Reference – Testování ASP.NET aplikací Core MVC