Sdílet prostřednictvím


Povolení automatického testování jednotek

od Microsoftu

Stáhnout PDF

Toto je krok 12 bezplatného kurzu aplikace NerdDinner , který vás provede sestavením malé, ale kompletní webové aplikace pomocí ASP.NET MVC 1.

Krok 12 ukazuje, jak vyvinout sadu automatizovaných testů jednotek, které ověřují funkčnost nerdDinner a které nám dávají jistotu, že v budoucnu provedeme změny a vylepšení aplikace.

Pokud používáte ASP.NET MVC 3, doporučujeme postupovat podle kurzů Začínáme S MVC 3 nebo MVC Music Store.

NerdDinner Krok 12: Testování jednotek

Pojďme vyvinout sadu automatizovaných testů jednotek, které ověří funkčnost nerdDinneru a které nám poskytnou jistotu, že v budoucnu provedeme změny a vylepšení aplikace.

Proč test jednotek?

Na cestě do práce jednoho rána máte najednou blesk inspirace ohledně aplikace, na které pracujete. Uvědomujete si, že existuje změna, kterou můžete implementovat a která aplikaci výrazně zlepší. Může se jednat o refaktoring, který kód vyčistí, přidá novou funkci nebo opraví chybu.

Otázka, která se vám při příchodu k počítači zobrazí, zní: "Jak bezpečné je toto vylepšení provést?" Co když má změna vedlejší účinky nebo něco pokazí? Změna může být jednoduchá a může trvat jen pár minut, ale co když ruční otestování všech scénářů aplikace trvá hodiny? Co když zapomenete pokrýt scénář a poškozená aplikace přejde do produkčního prostředí? Stojí toto vylepšení opravdu za veškeré úsilí?

Automatizované testy jednotek poskytují bezpečnostní síť, která vám umožní neustále vylepšovat vaše aplikace a vyhnout se obavám z kódu, na který pracujete. Automatizované testy, které rychle ověřují funkčnost, vám umožní kódovat s jistotou – a umožní vám dělat vylepšení, která byste jinak nemuseli dělat. Pomáhají také vytvářet řešení, která jsou lépe udržovatelná a mají delší životnost, což vede k mnohem vyšší návratnosti investic.

Architektura ASP.NET MVC usnadňuje a přirozeně testuje funkce aplikací. Umožňuje také pracovní postup TDD (Test Driven Development), který umožňuje vývoj založený na testování.

Projekt NerdDinner.Tests

Když jsme na začátku tohoto kurzu vytvořili aplikaci NerdDinner, zobrazilo se nám dialogové okno s dotazem, jestli chceme vytvořit projekt testování jednotek, který bude postupovat společně s projektem aplikace:

Snímek obrazovky s dialogovým oknem Vytvořit projekt testu jednotek Ano, je vybraný projekt vytvořit test jednotek. Nerd Dinner dot Tests je napsaný jako název testovacího projektu.

Ponechali jsme vybraný přepínač "Ano, vytvořit projekt testování jednotek", což vedlo k přidání projektu NerdDinner.Tests do našeho řešení:

Snímek obrazovky s navigačním stromem Průzkumník řešení Je vybraná možnost Nerd Dinner dot Tests.

Projekt NerdDinner.Tests odkazuje na sestavení projektu aplikace NerdDinner a umožňuje nám do něj snadno přidat automatizované testy, které ověřují funkčnost aplikace.

Vytváření testů jednotek pro naši třídu modelu Večeře

Pojďme do projektu NerdDinner.Tests přidat několik testů, které ověřují třídu Dinner, kterou jsme vytvořili při vytváření vrstvy modelu.

Začneme vytvořením nové složky v rámci našeho testovacího projektu s názvem "Modely", kam umístíme testy související s modelem. Potom klikneme pravým tlačítkem na složku a zvolíme příkaz Nabídky Přidat> nový test . Tím se zobrazí dialogové okno Přidat nový test.

Zvolíme vytvoření testu jednotek a pojmenujeme ho "DinnerTest.cs":

Snímek obrazovky s dialogovým oknem Přidat nový test Test jednotek je zvýrazněný. Dinner Test dot c s se zapíše jako název testu.

Když klikneme na tlačítko "OK", Visual Studio do projektu přidá (a otevře) soubor DinnerTest.cs:

Snímek obrazovky se souborem Dinner Test dot c s v sadě Visual Studio

Výchozí šablona testu jednotek v sadě Visual Studio obsahuje spoustu kódu, který je trochu nepřehledný. Pojďme ho vyčistit tak, aby obsahoval pouze následující kód:

using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NerdDinner.Models;

namespace NerdDinner.Tests.Models {
 
    [TestClass]
    public class DinnerTest {

    }
}

Atribut [TestClass] ve třídě DinnerTest výše ji identifikuje jako třídu, která bude obsahovat testy, stejně jako volitelnou inicializaci testu a kód odtrží. Testy v něm můžeme definovat přidáním veřejných metod, které mají atribut [TestMethod].

Níže jsou první ze dvou testů, které přidáme, aby se cvičily ve třídě Večeře. První test ověří, že je naše večeře neplatná, pokud se vytvoří nová večeře bez správného nastavení všech vlastností. Druhý test ověří, že je naše večeře platná, pokud má večeře všechny vlastnosti nastavené s platnými hodnotami:

[TestClass]
public class DinnerTest {

    [TestMethod]
    public void Dinner_Should_Not_Be_Valid_When_Some_Properties_Incorrect() {

        //Arrange
        Dinner dinner = new Dinner() {
            Title = "Test title",
            Country = "USA",
            ContactPhone = "BOGUS"
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsFalse(isValid);
    }

    [TestMethod]
    public void Dinner_Should_Be_Valid_When_All_Properties_Correct() {
        
        //Arrange
        Dinner dinner = new Dinner {
            Title = "Test title",
            Description = "Some description",
            EventDate = DateTime.Now,
            HostedBy = "ScottGu",
            Address = "One Microsoft Way",
            Country = "USA",
            ContactPhone = "425-703-8072",
            Latitude = 93,
            Longitude = -92,
        };

        // Act
        bool isValid = dinner.IsValid;

        //Assert
        Assert.IsTrue(isValid);
    }
}

Všimněte si, že názvy testů jsou velmi explicitní (a poněkud podrobné). Děláme to proto, že bychom mohli vytvořit stovky nebo tisíce malých testů a chceme, aby bylo snadné rychle určit záměr a chování každého z nich (zejména při procházení seznamu selhání ve runneru testů). Názvy testů by měly být pojmenovány podle funkce, kterou testují. Výše používáme vzor pojmenování "Noun_Should_Verb".

Testy strukturujeme pomocí vzoru testování "AAA" – což je zkratka pro "Arrange, Act, Assert":

  • Uspořádat: Nastavení testované jednotky
  • Akce: Procvičte si jednotku v rámci testu a zachyťte výsledky.
  • Assert: Ověření chování

Když píšeme testy, chceme se vyhnout tomu, aby jednotlivé testy dělaly příliš mnoho. Místo toho by měl každý test ověřit pouze jeden koncept (což výrazně usnadní určení příčiny selhání). Dobrým vodítkem je zkusit a mít pro každý test jenom jeden příkaz Assert. Pokud máte v testovací metodě více než jeden příkaz assert, ujistěte se, že se všechny používají k otestování stejného konceptu. V případě pochybností proveďte další test.

Spouštění testů

Visual Studio 2008 Professional (a vyšší edice) obsahuje integrovaný test runner, který se dá použít ke spouštění projektů Visual Studio Unit Test v integrovaném vývojovém prostředí. Všechny testy jednotek spustíme tak, že vybereme příkaz nabídky Test-Run-All>> Tests in Solution (nebo zadáte Ctrl R, A). Případně můžeme kurzor umístit do konkrétní testovací třídy nebo testovací metody a pomocí příkazu Test-Run-Tests>> v aktuální místní nabídce (nebo zadat Ctrl R, T) spustit podmnožinu testů jednotek.

Pojďme umístit kurzor do třídy DinnerTest a zadáním "Ctrl R, T" spustit dva testy, které jsme právě definovali. Když to uděláme, zobrazí se v sadě Visual Studio okno Výsledky testu a v něm se zobrazí výsledky našeho testovacího běhu:

Snímek obrazovky s oknem Výsledky testu v sadě Visual Studio Výsledky testovacího běhu jsou uvedené v části .

Poznámka: V okně výsledků testu VS se ve výchozím nastavení nezobrazuje sloupec Název třídy. Můžete ho přidat tak, že kliknete pravým tlačítkem v okně Výsledky testu a použijete příkaz nabídky Přidat nebo odebrat sloupce.

Naše dva testy trvaly jen zlomek sekundy, a jak vidíte, oba prošly. Teď je můžeme rozšířit vytvořením dalších testů, které ověřují ověření konkrétních pravidel a zahrnují dvě pomocné metody IsUserHost() a IsUserRegistered(), které jsme přidali do třídy Dinner. Všechny tyto testy pro třídu Dinner budou v budoucnu mnohem jednodušší a bezpečnější přidávat do ní nová obchodní pravidla a ověřování. Novou logiku pravidla můžeme přidat do aplikace Dinner a během několika sekund ověřit, že nenarušila žádnou z předchozích funkcí logiky.

Všimněte si, jak použití popisného názvu testu usnadňuje rychlé pochopení toho, co jednotlivé testy ověřují. Doporučuji použít příkaz nabídky Nástroje-Možnosti>, otevřít konfigurační obrazovku Test Tools-Test> Execution a zaškrtnout políčko "Poklikání na neúspěšný nebo neprůkazný výsledek testu jednotek zobrazí bod selhání v testu". To vám umožní poklikat na selhání v okně výsledků testu a okamžitě přejít na selhání kontrolního příkazu.

Creating DinnersController Unit Tests

Pojďme teď vytvořit několik testů jednotek, které ověřují funkčnost zařízení DinnersController. Začneme tak, že klikneme pravým tlačítkem na složku Kontrolery v rámci našeho projektu Test a pak zvolíme příkaz Nabídky Přidat nový> test . Vytvoříme test jednotek a pojmenujeme ho "DinnersControllerTest.cs".

Vytvoříme dvě testovací metody, které ověří metodu akce Details() na zařízení DinnersController. První ověří, že zobrazení se vrátí, když se požádá o existující večeři. Druhý ověří, že se při požadavku na neexistující večeři vrátí zobrazení Nenalezeno:

[TestClass]
public class DinnersControllerTest {

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_ExistingDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(1) as ViewResult;

        // Assert
        Assert.IsNotNull(result, "Expected View");
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = new DinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

        // Assert
        Assert.AreEqual("NotFound", result.ViewName);
    } 
}

Výše uvedený kód se zkompiluje dočista. Když ale testy spustíme, oba selžou:

Snímek obrazovky s kódem Oba testy selhaly.

Když se podíváme na chybové zprávy, zjistíme, že důvodem selhání testů bylo to, že se naše třída DinnersRepository nemohla připojit k databázi. Naše aplikace NerdDinner používá připojovací řetězec k místnímu souboru SQL Server Express, který se nachází v adresáři \App_Data projektu aplikace NerdDinner. Vzhledem k tomu, že se projekt NerdDinner.Tests zkompiluje a spouští v jiném adresáři než projekt aplikace, je relativní umístění cesty našeho připojovacího řetězce nesprávné.

Mohli bychom to vyřešit zkopírováním souboru databáze SQL Express do našeho testovacího projektu a následným přidáním odpovídajícího připojovacího řetězce testu do App.config našeho testovacího projektu. Tím by se výše uvedené testy odblokovaly a spustily.

Kód testování jednotek využívající skutečnou databázi ale přináší řadu výzev. Konkrétně se jedná o tyto:

  • Výrazně se tím zpomalí doba provádění testů jednotek. Čím déle trvá spuštění testů, tím menší je pravděpodobnost, že je budete spouštět často. V ideálním případě chcete, aby testy jednotek mohly běžet během několika sekund – a měly by to být něco, co děláte stejně přirozeně jako kompilaci projektu.
  • Komplikuje logiku nastavení a čištění v rámci testů. Chcete, aby byl každý test jednotek izolovaný a nezávislý na ostatních (bez vedlejších účinků nebo závislostí). Při práci se skutečnou databází musíte mít na paměti stav a mezi testy ho resetovat.

Podívejme se na návrhový vzor s názvem "injektáž závislostí", který nám může pomoct tyto problémy obejít a vyhnout se nutnosti používat skutečnou databázi s našimi testy.

Injektáž závislostí

Právě teď je DinnersController pevně "svázán" s třídou DinnerRepository. "Spojka" označuje situaci, kdy třída explicitně spoléhá na jinou třídu, aby fungovala:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    //
    // GET: /Dinners/Details/5

    public ActionResult Details(int id) {

        Dinner dinner = dinnerRepository.FindDinner(id);

        if (dinner == null)
            return View("NotFound");

        return View(dinner);
    }

Vzhledem k tomu, DinnerRepository třídy vyžaduje přístup k databázi, úzce svázané závislosti DinnersController třídy na DinnerRepository nakonec vyžaduje, abychom měli databázi pro DinnersController metody akce být testovány.

Tento problém můžeme obejít použitím vzoru návrhu označovaného jako "injektáž závislostí", což je přístup, kdy se už implicitně nevytvářejí závislosti (jako třídy úložiště, které poskytují přístup k datům) v rámci tříd, které je používají. Místo toho lze závislosti explicitně předat třídě, která je používá, pomocí argumentů konstruktoru. Pokud jsou závislosti definovány pomocí rozhraní, máme pak možnost předávat "falešné" implementace závislostí pro scénáře testování částí. To nám umožňuje vytvářet implementace závislostí specifické pro testování, které ve skutečnosti nevyžadují přístup k databázi.

Abychom to viděli v praxi, implementujeme injektáž závislostí pomocí třídy DinnersController.

Extrahování rozhraní IDinnerRepository

Prvním krokem bude vytvoření nového rozhraní IDinnerRepository, které zapouzdřuje kontrakt úložiště, který naše kontrolery vyžadují k načtení a aktualizaci aplikace Dinners.

Tento kontrakt rozhraní můžeme definovat ručně tak, že kliknete pravým tlačítkem na složku \Models, zvolíme příkaz nabídky Přidat> novou položku a vytvoříme nové rozhraní s názvem IDinnerRepository.cs.

Případně můžeme použít nástroje refaktoringu integrované do Visual Studio Professional (a vyšších edic) k automatické extrakci a vytvoření rozhraní z naší stávající třídy DinnerRepository. Chcete-li extrahovat toto rozhraní pomocí sady Visual Studio, jednoduše umístěte kurzor v textovém editoru na třídu DinnerRepository a pak klikněte pravým tlačítkem a zvolte příkaz nabídky Refactor-Extract> Interface :

Snímek obrazovky znázorňující vybranou možnost Extrahovat rozhraní v podnabídce Refaktoring

Tím se spustí dialogové okno "Extrahovat rozhraní" a zobrazí se výzva k zadání názvu rozhraní, které chcete vytvořit. Ve výchozím nastavení se použije IDinnerRepository a automaticky se vyberou všechny veřejné metody v existující třídě DinnerRepository, které se přidají do rozhraní:

Snímek obrazovky s oknem Výsledky testu v sadě Visual Studio

Když klikneme na tlačítko "ok", Visual Studio přidá do naší aplikace nové rozhraní IDinnerRepository:

public interface IDinnerRepository {

    IQueryable<Dinner> FindAllDinners();
    IQueryable<Dinner> FindByLocation(float latitude, float longitude);
    IQueryable<Dinner> FindUpcomingDinners();
    Dinner             GetDinner(int id);

    void Add(Dinner dinner);
    void Delete(Dinner dinner);
    
    void Save();
}

A naše stávající třída DinnerRepository se aktualizuje tak, aby implementovali rozhraní :

public class DinnerRepository : IDinnerRepository {
   ...
}

Aktualizace třídy DinnersController pro podporu injektáže konstruktoru

Teď aktualizujeme třídu DinnersController tak, aby používala nové rozhraní.

V současné době je DinnersController pevně zakódován tak, že jeho pole "dinnerRepository" je vždy třída DinnerRepository:

public class DinnersController : Controller {

    DinnerRepository dinnerRepository = new DinnerRepository();

    ...
}

Změníme ho tak, aby pole "dinnerRepository" bylo typu IDinnerRepository a ne DinnerRepository. Pak přidáme dva veřejné konstruktory DinnersController. Jeden z konstruktorů umožňuje předat IDinnerRepository jako argument. Druhý je výchozí konstruktor, který používá naši existující implementaci DinnerRepository:

public class DinnersController : Controller {

    IDinnerRepository dinnerRepository;

    public DinnersController()
        : this(new DinnerRepository()) {
    }

    public DinnersController(IDinnerRepository repository) {
        dinnerRepository = repository;
    }
    ...
}

Vzhledem k tomu, že ASP.NET MVC ve výchozím nastavení vytváří třídy kontroleru pomocí výchozích konstruktorů, bude náš DinnersController za běhu dál používat třídu DinnerRepository k provedení přístupu k datům.

Teď ale můžeme naše testy jednotek aktualizovat tak, aby předaly implementaci "falešného" úložiště dinner pomocí konstruktoru parametrů. Toto "falešné" úložiště večeře nebude vyžadovat přístup ke skutečné databázi a místo toho bude používat ukázková data v paměti.

Vytvoření třídy FakeDinnerRepository

Pojďme vytvořit třídu FakeDinnerRepository.

Začneme vytvořením adresáře Fakes v rámci našeho projektu NerdDinner.Tests a pak do něj přidáme novou třídu FakeDinnerRepository (klikněte pravým tlačítkem na složku a zvolte Add-New> Class):

Snímek obrazovky s položkou nabídky Přidat novou třídu Možnost Přidat novou položku je zvýrazněná.

Aktualizujeme kód tak, aby třída FakeDinnerRepository implementuje rozhraní IDinnerRepository. Pak na něj můžeme kliknout pravým tlačítkem a zvolit příkaz místní nabídky "Implementace rozhraní IDinnerRepository":

Snímek obrazovky s příkazem místní nabídky Implement interface I Dinner Repository

To způsobí, že Visual Studio automaticky přidá všechny členy rozhraní IDinnerRepository do třídy FakeDinnerRepository s výchozí implementací "stub out":

public class FakeDinnerRepository : IDinnerRepository {

    public IQueryable<Dinner> FindAllDinners() {
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float long){
        throw new NotImplementedException();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        throw new NotImplementedException();
    }

    public Dinner GetDinner(int id) {
        throw new NotImplementedException();
    }

    public void Add(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Delete(Dinner dinner) {
        throw new NotImplementedException();
    }

    public void Save() {
        throw new NotImplementedException();
    }
}

Pak můžeme aktualizovat implementaci FakeDinnerRepository tak, aby pracovala s kolekcí seznamu Dinner<> v paměti, která se jí předala jako argument konstruktoru:

public class FakeDinnerRepository : IDinnerRepository {

    private List<Dinner> dinnerList;

    public FakeDinnerRepository(List<Dinner> dinners) {
        dinnerList = dinners;
    }

    public IQueryable<Dinner> FindAllDinners() {
        return dinnerList.AsQueryable();
    }

    public IQueryable<Dinner> FindUpcomingDinners() {
        return (from dinner in dinnerList
                where dinner.EventDate > DateTime.Now
                select dinner).AsQueryable();
    }

    public IQueryable<Dinner> FindByLocation(float lat, float lon) {
        return (from dinner in dinnerList
                where dinner.Latitude == lat && dinner.Longitude == lon
                select dinner).AsQueryable();
    }

    public Dinner GetDinner(int id) {
        return dinnerList.SingleOrDefault(d => d.DinnerID == id);
    }

    public void Add(Dinner dinner) {
        dinnerList.Add(dinner);
    }

    public void Delete(Dinner dinner) {
        dinnerList.Remove(dinner);
    }

    public void Save() {
        foreach (Dinner dinner in dinnerList) {
            if (!dinner.IsValid)
                throw new ApplicationException("Rule violations");
        }
    }
}

Teď máme falešnou implementaci IDinnerRepository, která nevyžaduje databázi a může místo toho vytvořit seznam objektů Dinner v paměti.

Použití FakeDinnerRepository s testy jednotek

Vraťme se k testům jednotek DinnersController, které dříve selhaly, protože databáze nebyla k dispozici. Pomocí následujícího kódu můžeme aktualizovat testovací metody tak, aby používaly FakeDinnerRepository naplněné ukázkovými daty Dinner v paměti pro DinnersController:

[TestClass]
public class DinnersControllerTest {

    List<Dinner> CreateTestDinners() {

        List<Dinner> dinners = new List<Dinner>();

        for (int i = 0; i < 101; i++) {

            Dinner sampleDinner = new Dinner() {
                DinnerID = i,
                Title = "Sample Dinner",
                HostedBy = "SomeUser",
                Address = "Some Address",
                Country = "USA",
                ContactPhone = "425-555-1212",
                Description = "Some description",
                EventDate = DateTime.Now.AddDays(i),
                Latitude = 99,
                Longitude = -99
            };
            
            dinners.Add(sampleDinner);
        }
        
        return dinners;
    }

    DinnersController CreateDinnersController() {
        var repository = new FakeDinnerRepository(CreateTestDinners());
        return new DinnersController(repository);
    }

    [TestMethod]
    public void DetailsAction_Should_Return_View_For_Dinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(1);

        // Assert
        Assert.IsInstanceOfType(result, typeof(ViewResult));
    }

    [TestMethod]
    public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner() {

        // Arrange
        var controller = CreateDinnersController();

        // Act
        var result = controller.Details(999) as ViewResult;

        // Assert
        Assert.AreEqual("NotFound", result.ViewName);
    }
}

A teď, když spustíme tyto testy, oba projdou:

Snímek obrazovky s testy jednotek, oba testy prošly

A co je nejlepší, jejich spuštění trvá jen zlomek sekundy a nevyžadují žádnou složitou logiku nastavení/čištění. Teď můžeme testovat všechny naše metody akcí DinnersController (včetně výpisu, stránkování, podrobností, vytvoření, aktualizace a odstranění), aniž bychom se museli připojovat ke skutečné databázi.

Vedlejší téma: Architektury injektáže závislostí
Provedení ručního injektáže závislostí (jako je výše) funguje bez problémů, ale s rostoucím počtem závislostí a komponent v aplikaci je jejich údržba obtížnější. Pro .NET existuje několik architektur injektáže závislostí, které můžou pomoct zajistit ještě větší flexibilitu správy závislostí. Tato rozhraní, někdy označovaná také jako kontejnery IoC (Inversion of Control), poskytují mechanismy, které umožňují další úroveň podpory konfigurace pro zadávání a předávání závislostí do objektů za běhu (nejčastěji pomocí injektáže konstruktoru). Mezi nejoblíbenější architektury OSS Dependency Injection / IOC v .NET patří: AutoFac, Ninject, Spring.NET, StructureMap a Windsor. ASP.NET MVC zpřístupňuje rozhraní API rozšiřitelnosti, která vývojářům umožňují podílet se na překladu a vytváření instancí kontrolerů a která umožňují do tohoto procesu čistě integrovat architektury Injektáž závislostí / IoC. Použití architektury DI/IOC by nám také umožnilo odebrat výchozí konstruktor z našeho DinnersController , což by zcela odebralo spojení mezi ním a DinnerRepository. U naší aplikace NerdDinner nebudeme používat architekturu injektáže závislostí / IOC. Je to ale něco, co bychom mohli zvážit v budoucnu, pokud by se základ kódu a možnosti nerdDinner zvětšily.

Vytváření testů upravit akční jednotky

Pojďme teď vytvořit několik testů jednotek, které ověřují funkci Edit aplikace DinnersController. Začneme tím, že otestujeme verzi HTTP-GET akce Upravit:

//
// GET: /Dinners/Edit/5

[Authorize]
public ActionResult Edit(int id) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    return View(new DinnerFormViewModel(dinner));
}

Vytvoříme test, který ověří, jestli se zobrazení zálohované objektem DinnerFormViewModel vykreslí zpět, když se požádá o platnou večeři:

[TestMethod]
public void EditAction_Should_Return_View_For_ValidDinner() {

    // Arrange
    var controller = CreateDinnersController();

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

Když ale test spustíme, zjistíme, že selže, protože při přístupu metody Edit k vlastnosti User.Identity.Name za účelem provedení kontroly Dinner.IsHostedBy() dojde k výjimce nulového odkazu.

Objekt User v základní třídě Controller zapouzdřuje podrobnosti o přihlášeném uživateli a je naplněn ASP.NET MVC při vytváření kontroleru za běhu. Vzhledem k tomu, že dinnersController testujeme mimo prostředí webového serveru, objekt User není nastavený (proto se jedná o výjimku s nulovým odkazem).

Napodobení vlastnosti User.Identity.Name

Napodobování architektur usnadňuje testování tím, že nám umožňuje dynamicky vytvářet falešné verze závislých objektů, které podporují naše testy. Pomocí napodobení architektury v testu akce Upravit můžeme například dynamicky vytvořit objekt User, který může ovládací prvek DinnersController použít k vyhledání simulovaného uživatelského jména. Tím zabráníte vyvolání nulového odkazu při spuštění testu.

S ASP.NET MVC je možné použít mnoho architektur napodobování .NET (jejich seznam najdete tady: http://www.mockframeworks.com/).

Po stažení přidáme odkaz na sestavení Moq.dll v projektu NerdDinner.Tests:

Snímek obrazovky s navigačním stromem Nerd Dinner Moq je zvýrazněno.

Pak do naší testovací třídy přidáme pomocnou metodu CreateDinnersControllerAs(username), která přebírá uživatelské jméno jako parametr a která pak "napodobí" vlastnost User.Identity.Name v instanci DinnersController:

DinnersController CreateDinnersControllerAs(string userName) {

    var mock = new Mock<ControllerContext>();
    mock.SetupGet(p => p.HttpContext.User.Identity.Name).Returns(userName);
    mock.SetupGet(p => p.HttpContext.Request.IsAuthenticated).Returns(true);

    var controller = CreateDinnersController();
    controller.ControllerContext = mock.Object;

    return controller;
}

Výše používáme Moq k vytvoření objektu Napodobení, který vytváří objekt ControllerContext (což je to, co ASP.NET MVC předává třídám controlleru, aby zpřístupnil objekty modulu runtime, jako je User, Request, Response a Session). Voláme metodu "SetupGet" na napodobeninu, abychom označili, že vlastnost HttpContext.User.Identity.Name na ControllerContext by měla vrátit řetězec uživatelského jména, který jsme předali pomocné metodě.

Můžeme napodobeninu libovolného počtu vlastností a metod ControllerContext. Abychom to ilustrovali, přidali jsme také volání SetupGet() pro vlastnost Request.IsAuthenticated (které ve skutečnosti není pro níže uvedené testy potřeba – ale pomáhá ilustrovat, jak můžete napodobit vlastnosti požadavku). Až skončíme, přiřadíme instanci modelu ControllerContext k objektu DinnersController, který vrátí naše pomocná metoda.

Teď můžeme psát testy jednotek, které používají tuto pomocnou metodu k testování scénářů úprav zahrnujících různé uživatele:

[TestMethod]
public void EditAction_Should_Return_EditView_When_ValidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}

[TestMethod]
public void EditAction_Should_Return_InvalidOwnerView_When_InvalidOwner() {

    // Arrange
    var controller = CreateDinnersControllerAs("NotOwnerUser");

    // Act
    var result = controller.Edit(1) as ViewResult;

    // Assert
    Assert.AreEqual(result.ViewName, "InvalidOwner");
}

A teď, když spustíme testy, které projdou:

Snímek obrazovky s testy jednotek, které používají pomocnou metodu Testy byly úspěšné.

Testování scénářů UpdateModel()

Vytvořili jsme testy, které pokrývají verzi HTTP-GET akce Upravit. Teď vytvoříme několik testů, které ověří verzi HTTP-POST akce Upravit:

//
// POST: /Dinners/Edit/5

[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Edit (int id, FormCollection collection) {

    Dinner dinner = dinnerRepository.GetDinner(id);

    if (!dinner.IsHostedBy(User.Identity.Name))
        return View("InvalidOwner");

    try {
        UpdateModel(dinner);

        dinnerRepository.Save();

        return RedirectToAction("Details", new { id=dinner.DinnerID });
    }
    catch {
        ModelState.AddModelErrors(dinner.GetRuleViolations());
        
        return View(new DinnerFormViewModel(dinner));
    }
}

Zajímavým novým testovacím scénářem pro podporu této metody akce je použití pomocné metody UpdateModel() v základní třídě Controller. Tuto pomocnou metodu používáme k vytvoření vazby hodnot form-post na instanci objektu Dinner.

Níže jsou dva testy, které ukazují, jak můžeme zadat zaúčtované hodnoty pro metodu pomocné rutiny UpdateModel(), která se má použít. Provedeme to tak, že vytvoříme a naplníme objekt FormCollection a pak ho přiřadíme k vlastnosti ValueProvider na kontroleru.

První test ověří, že při úspěšném uložení se prohlížeč přesměruje na akci podrobností. Druhý test ověří, že při publikování neplatného vstupu akce znovu zobrazí zobrazení pro úpravy s chybovou zprávou.

[TestMethod]
public void EditAction_Should_Redirect_When_Update_Successful() {

    // Arrange      
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "Title", "Another value" },
        { "Description", "Another description" }
    };

    controller.ValueProvider = formValues.ToValueProvider();
    
    // Act
    var result = controller.Edit(1, formValues) as RedirectToRouteResult;

    // Assert
    Assert.AreEqual("Details", result.RouteValues["Action"]);
}

[TestMethod]
public void EditAction_Should_Redisplay_With_Errors_When_Update_Fails() {

    // Arrange
    var controller = CreateDinnersControllerAs("SomeUser");

    var formValues = new FormCollection() {
        { "EventDate", "Bogus date value!!!"}
    };

    controller.ValueProvider = formValues.ToValueProvider();

    // Act
    var result = controller.Edit(1, formValues) as ViewResult;

    // Assert
    Assert.IsNotNull(result, "Expected redisplay of view");
    Assert.IsTrue(result.ViewData.ModelState.Count > 0, "Expected errors");
}

Testovací Wrap-Up

Probrali jsme základní koncepty zahrnuté do tříd kontroleru testování jednotek. Tyto techniky můžeme použít ke snadnému vytvoření stovek jednoduchých testů, které ověřují chování naší aplikace.

Vzhledem k tomu, že naše testy kontroleru a modelu nevyžadují skutečnou databázi, jsou extrémně rychlé a snadno se spouštějí. Během několika sekund budeme moct spustit stovky automatizovaných testů a okamžitě získat zpětnou vazbu, jestli nějaká změna něco nepokazila. To nám pomůže zajistit jistotu, že budeme naši aplikaci neustále vylepšovat, refaktorovat a vylepšovat.

Testování jsme probrali jako poslední téma v této kapitole – ale ne proto, že testování je něco, co byste měli udělat na konci procesu vývoje. Naopak byste měli co nejdříve psát automatizované testy ve vývojovém procesu. To vám umožní získat okamžitou zpětnou vazbu při vývoji, pomůže vám promyšleně přemýšlet o scénářích použití vaší aplikace a povede vás při návrhu aplikace s ohledem na čisté vrstvení a propojení.

V pozdější kapitole této knihy se budeme zabývat vývojem řízeným testy (TDD) a jeho používáním s ASP.NET MVC. TDD je iterativní postup kódování, při kterém nejprve napíšete testy, které výsledný kód vyhovuje. S TDD začnete každou funkci vytvořením testu, který ověří funkce, které se chystáte implementovat. Napsání testu jednotek jako první vám pomůže zajistit, že funkci jasně rozumíte a jak by měla fungovat. Teprve po napsání testu (a ověření, že selhal) implementujete vlastní funkce, které test ověří. Vzhledem k tomu, že jste už strávili čas přemýšlením o tom, jak má funkce fungovat, budete lépe rozumět požadavkům a tomu, jak je co nejlépe implementovat. Po dokončení implementace můžete test spustit znovu a získat okamžitou zpětnou vazbu, jestli funkce funguje správně. Více se budeme věnovat tématu TDD v kapitole 10.

Další krok

Některé závěrečné komentáře.