Iterace č. 5 – vytvoření testů jednotek (VB)
od Microsoftu
V páté iteraci usnadňujeme údržbu a úpravy naší aplikace přidáním testů jednotek. Vysmíváme si třídy datového modelu a sestavujeme testy jednotek pro naše kontrolery a logiku ověřování.
Vytvoření aplikace Správy kontaktů ASP.NET MVC (VB)
V této sérii kurzů sestavíme od začátku do konce celou aplikaci Pro správu kontaktů. Aplikace Contact Manager umožňuje ukládat kontaktní informace – jména, telefonní čísla a e-mailové adresy – pro seznam osob.
Aplikaci sestavíme pomocí několika iterací. S každou iterací aplikaci postupně vylepšujeme. Cílem tohoto přístupu s více iteracemi je pochopit důvod každé změny.
Iterace č. 1 – vytvořte aplikaci. V první iteraci vytvoříme Správce kontaktů nejjednodušším možným způsobem. Přidáváme podporu základních databázových operací: vytvoření, čtení, aktualizace a odstranění (CRUD).
Iterace č. 2 – vzhled aplikace V této iteraci vylepšujeme vzhled aplikace úpravou výchozí ASP.NET stránky předlohy zobrazení MVC a šablony stylů CSS.
Iterace č. 3 – přidání ověření formuláře Ve třetí iteraci přidáme základní ověření formuláře. Bráníme uživatelům v odeslání formuláře bez vyplnění požadovaných polí formuláře. Ověřujeme také e-mailové adresy a telefonní čísla.
Iterace č. 4 – Nastavte aplikaci volně na párování. V této čtvrté iteraci využijeme několik vzorů návrhu softwaru, které usnadňují údržbu a úpravy aplikace Contact Manager. Například refaktorujeme aplikaci tak, aby používala vzor úložiště a model injektáže závislostí.
Iterace č. 5 – vytvoření testů jednotek V páté iteraci usnadňujeme údržbu a úpravy naší aplikace přidáním testů jednotek. Vysmíváme si třídy datového modelu a sestavujeme testy jednotek pro naše kontrolery a logiku ověřování.
Iterace č. 6 – použijte vývoj řízený testy. V této šesté iteraci přidáme do naší aplikace nové funkce tím, že nejprve napíšeme testy jednotek a proti testům jednotek napíšeme kód. V této iteraci přidáme skupiny kontaktů.
Iterace č. 7 – přidání funkcí Ajax V sedmé iteraci vylepšujeme rychlost odezvy a výkon naší aplikace přidáním podpory pro Ajax.
Tato iterace
V předchozí iteraci aplikace Contact Manager jsme aplikaci refaktorovali tak, aby byla volněji svázána. Aplikaci jsme rozdělili do různých vrstev kontroleru, služby a úložiště. Každá vrstva komunikuje s vrstvou pod ní prostřednictvím rozhraní.
Refaktorovali jsme aplikaci, aby se usnadnila údržba a úpravy aplikace. Pokud například potřebujeme použít novou technologii přístupu k datům, můžeme jednoduše změnit vrstvu úložiště, aniž bychom se dotkli vrstvy kontroleru nebo služby. Díky volně propojenému Správci kontaktů jsme aplikaci udělali odolnější vůči změnám.
Co se ale stane, když potřebujeme do aplikace Contact Manager přidat novou funkci? Nebo co se stane, když chybu opravíme? Smutnou, ale osvědčenou pravdou psaní kódu je, že pokaždé, když se dotknete kódu, vytváříte riziko zavedení nových chyb.
Jednoho dne vás třeba nadřízený může požádat o přidání nové funkce do Správce kontaktů. Chce, abyste přidali podporu pro skupiny kontaktů. Chce, abyste uživatelům umožnili uspořádat si kontakty do skupin, jako jsou Přátelé, Firma atd.
Abyste mohli tuto novou funkci implementovat, budete muset upravit všechny tři vrstvy aplikace Contact Manager. Budete muset přidat nové funkce do kontrolerů, vrstvy služby a úložiště. Jakmile začnete upravovat kód, riskujete narušení funkčnosti, která dříve fungovala.
Refaktoring naší aplikace do samostatných vrstev, jako jsme to udělali v předchozí iteraci, byla dobrá věc. Bylo to dobré, protože nám umožňuje provádět změny v celých vrstvách, aniž by se to dotklo zbytku aplikace. Pokud ale chcete kód v rámci vrstvy snadněji udržovat a upravovat, musíte pro tento kód vytvořit testy jednotek.
Test jednotek se používá k otestování jednotlivých jednotek kódu. Tyto jednotky kódu jsou menší než celé aplikační vrstvy. Obvykle se používá test jednotek k ověření, jestli se určitá metoda v kódu chová očekávaným způsobem. Například byste vytvořili test jednotek pro metodu CreateContact() vystavenou třídou ContactManagerService.
Testy jednotek pro aplikaci fungují stejně jako bezpečnostní síť. Pokaždé, když upravíte kód v aplikaci, můžete spustit sadu testů jednotek a zkontrolovat, jestli úpravy naruší stávající funkce. Testy jednotek umožňují bezpečné úpravy kódu. Díky testům jednotek je veškerý kód ve vaší aplikaci odolnější vůči změnám.
V této iteraci přidáme testy jednotek do aplikace Contact Manager. V další iteraci tak můžeme do aplikace přidat skupiny kontaktů, aniž bychom se museli starat o narušení stávajících funkcí.
Poznámka
Existuje celá řada architektur pro testování jednotek, včetně NUnit, xUnit.net a MbUnit. V tomto kurzu používáme architekturu pro testování částí, která je součástí sady Visual Studio. Stejně snadno byste ale mohli použít jednu z těchto alternativních architektur.
Co se testuje
V dokonalém světě by byl veškerý kód pokrytý testy jednotek. V dokonalém světě byste měli dokonalou bezpečnostní síť. Mohli byste upravit jakýkoli řádek kódu ve vaší aplikaci a okamžitě vědět, spuštěním testů jednotek, zda změna narušila stávající funkce.
Nežijeme však v dokonalém světě. V praxi se při psaní testů jednotek soustředíte na psaní testů pro obchodní logiku (například logiku ověřování). Konkrétně nepíšete testy jednotek pro logiku přístupu k datům nebo logiku zobrazení.
Aby byly testy jednotek užitečné, musí se provádět velmi rychle. Pro aplikaci můžete snadno nashromáždit stovky (nebo dokonce tisíce) testů jednotek. Pokud testy jednotek poběží dlouho, vyhnete se jejich provádění. Jinými slovy, dlouhotrvající testy jednotek nejsou pro každodenní účely kódování k ničemu.
Z tohoto důvodu obvykle nepíšete testy jednotek pro kód, který komunikuje s databází. Spuštění stovek testů jednotek pro živou databázi by bylo příliš pomalé. Místo toho napodobíte databázi a napíšete kód, který komunikuje s napodobenou databází (napodobování databáze si probereme níže).
Z podobného důvodu obvykle nepíšete testy jednotek pro zobrazení. Chcete-li otestovat zobrazení, musíte aktivovat webový server. Vzhledem k tomu, že spouštění webového serveru je poměrně pomalý proces, nedoporučuje se vytvářet testy jednotek pro vaše zobrazení.
Pokud vaše zobrazení obsahuje složitou logiku, měli byste zvážit přesun logiky do pomocných metod. Můžete psát testy jednotek pro pomocné metody, které se spouštějí bez spouštění webového serveru.
Poznámka
I když psaní testů pro logiku přístupu k datům nebo logiku zobrazení není při psaní testů jednotek vhodné, tyto testy můžou být velmi užitečné při sestavování funkčních nebo integračních testů.
Poznámka
ASP.NET Web Forms View Engine je MVC. I když Web Forms View Engine závisí na webovém serveru, jiné moduly zobrazení nemusí být.
Použití objektové architektury Napodobení
Při vytváření testů jednotek je téměř vždy potřeba využít architekturu Napodobení objektů. Architektura Mock Object umožňuje vytvářet napodobení a zástupné procedury pro třídy ve vaší aplikaci.
K vygenerování napodobené verze třídy úložiště můžete například použít architekturu Napodobení objektu. Tímto způsobem můžete v testech jednotek místo třídy skutečného úložiště použít třídu napodobené úložiště. Použití napodobeného úložiště vám umožní vyhnout se spouštění kódu databáze při provádění testu jednotek.
Visual Studio neobsahuje architekturu napodobení objektu. Pro rozhraní .NET Framework je však k dispozici několik komerčních a open source modelových architektur:
- Moq - Tato architektura je k dispozici pod licencí open source BSD. Moq si můžete stáhnout z https://code.google.com/p/moq/.
- Nosorožec Mocks - Tato architektura je k dispozici pod licencí open source BSD. Nosorožec Mocks si můžete stáhnout z http://ayende.com/projects/rhino-mocks.aspx.
- Typemock Izolátor – jedná se o komerční architekturu. Zkušební verzi si můžete stáhnout z webu http://www.typemock.com/.
V tomto kurzu jsem se rozhodl použít Moq. Stejně snadno ale můžete použít Nosorožec Mocks nebo Typemock Isolator k vytvoření objektů Napodobení pro aplikaci Contact Manager.
Než budete moct používat Moq, musíte provést následující kroky:
- .
- Před rozbalení stahování nezapomeňte kliknout pravým tlačítkem myši na soubor a kliknout na tlačítko Odblokovat (viz Obrázek 1).
- Rozbalte stažený soubor.
- Výběrem možnosti nabídky Projekt, Přidat odkaz na odkaz na sestavení Moq do testovacího projektu otevřete dialogové okno Přidat odkaz . Na kartě Procházet přejděte do složky, do které jste rozbalili Moq, a vyberte Moq.dll sestavení. Klikněte na tlačítko OK (viz Obrázek 2).
Obrázek 01: Odblokování Moq (kliknutím zobrazíte obrázek v plné velikosti)
Obrázek 02: Odkazy po přidání Moq (kliknutím zobrazíte obrázek v plné velikosti)
Vytváření testů jednotek pro vrstvu služby
Začněme vytvořením sady testů jednotek pro vrstvu služby aplikace Contact Manager. Tyto testy použijeme k ověření logiky ověřování.
V projektu ContactManager.Tests vytvořte novou složku s názvem Models. Pak klikněte pravým tlačítkem na složku Modely a vyberte Přidat, Nový test. Zobrazí se dialogové okno Přidat nový test na obrázku 3. Vyberte šablonu Test jednotek a pojmenujte nový test ContactManagerServiceTest.vb. Kliknutím na tlačítko OK přidejte nový test do testovacího projektu.
Poznámka
Obecně chcete, aby struktura složek testovacího projektu odpovídala struktuře složek projektu ASP.NET MVC. Například umístíte testy kontroleru do složky Kontrolery, testy modelů do složky Modely atd.
Obrázek 03: Models\ContactManagerServiceTest.cs(Kliknutím zobrazíte obrázek v plné velikosti)
Na začátku chceme otestovat metodu CreateContact() vystavenou třídou ContactManagerService. Vytvoříme následujících pět testů:
- CreateContact() – testy, které createContact() vrátí hodnotu true, když se metodě předá platný kontakt.
- CreateContactRequiredFirstName() – testuje, že se do stavu modelu přidá chybová zpráva, když se metodě CreateContact() předá kontakt s chybějícím křestným jménem.
- CreateContactRequiredLastName() – testuje, že se do stavu modelu přidá chybová zpráva, když se metodě CreateContact() předá kontakt s chybějícím příjmením.
- CreateContactInvalidPhone() – testuje, že se do stavu modelu přidá chybová zpráva, když se metodě CreateContact() předá kontakt s neplatným telefonním číslem.
- CreateContactInvalidEmail() – testuje, že se do stavu modelu přidá chybová zpráva, když se metodě CreateContact() předá kontakt s neplatnou e-mailovou adresou.
První test ověří, že platný kontakt negeneruje chybu ověření. Zbývající testy kontrolují jednotlivá ověřovací pravidla.
Kód pro tyto testy je obsažen ve výpisu 1.
Výpis 1 – Models\ContactManagerServiceTest.vb
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports Moq
Imports System.Web.Mvc
<TestClass()> _
Public Class ContactManagerServiceTest
Private _mockRepository As Mock(Of IContactManagerRepository)
Private _modelState As ModelStateDictionary
Private _service As IContactManagerService
<TestInitialize()> _
Public Sub Initialize()
_mockRepository = New Mock(Of IContactManagerRepository)()
_modelState = New ModelStateDictionary()
_service = New ContactManagerService(new ModelStateWrapper(_modelState), _mockRepository.Object)
End Sub
<TestMethod()> _
Public Sub CreateContact()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "steve@somewhere.com")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsTrue(result)
End Sub
<TestMethod()> _
Public Sub CreateContactRequiredFirstName()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, String.Empty, "Walther", "555-5555", "steve@somewhere.com")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsFalse(result)
Dim [error] = _modelState("FirstName").Errors(0)
Assert.AreEqual("First name is required.", [error].ErrorMessage)
End Sub
<TestMethod()> _
Public Sub CreateContactRequiredLastName()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, "Stephen", String.Empty, "555-5555", "steve@somewhere.com")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsFalse(result)
Dim [error] = _modelState("LastName").Errors(0)
Assert.AreEqual("Last name is required.", [error].ErrorMessage)
End Sub
<TestMethod()> _
Public Sub CreateContactInvalidPhone()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, "Stephen", "Walther", "apple", "steve@somewhere.com")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsFalse(result)
Dim [error] = _modelState("Phone").Errors(0)
Assert.AreEqual("Invalid phone number.", [error].ErrorMessage)
End Sub
<TestMethod()> _
Public Sub CreateContactInvalidEmail()
' Arrange
Dim contactToCreate = Contact.CreateContact(-1, "Stephen", "Walther", "555-5555", "apple")
' Act
Dim result = _service.CreateContact(contactToCreate)
' Assert
Assert.IsFalse(result)
Dim [error] = _modelState("Email").Errors(0)
Assert.AreEqual("Invalid email address.", [error].ErrorMessage)
End Sub
End Class
Vzhledem k tomu, že ve výpisu 1 používáme třídu Contact, musíme do projektu Test přidat odkaz na Microsoft Entity Framework. Přidejte odkaz na sestavení System.Data.Entity.
Výpis 1 obsahuje metodu s názvem Initialize(), která je opatřena atributem [TestInitialize]. Tato metoda je volána automaticky před spuštěním každého z testů jednotek (volá se 5krát těsně před každým testem jednotek). Metoda Initialize() vytvoří napodobenou úložiště s následujícím řádkem kódu:
_mockRepository = New Mock(Of IContactManagerRepository)()
Tento řádek kódu používá architekturu Moq ke generování napodobení úložiště z rozhraní IContactManagerRepository. Místo skutečné entityContactManagerRepository se používá napodobené úložiště, aby se při spuštění každého testu jednotek zabránilo přístupu k databázi. Napodobení úložiště implementuje metody rozhraní IContactManagerRepository, ale metody ve skutečnosti nic neudělají.
Poznámka
Při použití architektury Moq existuje rozdíl mezi _mockRepository a _mockRepository.Object. První odkazuje na Mock(Of IContactManagerRepository) třídy, která obsahuje metody pro určení, jak se bude napodobovat úložiště chovat. Druhý odkaz odkazuje na skutečné napodobení úložiště, které implementuje IContactManagerRepository rozhraní.
Napodobení úložiště se používá v metodě Initialize() při vytváření instance třídy ContactManagerService. Všechny jednotlivé testy jednotek používají tuto instanci Třídy ContactManagerService.
Seznam 1 obsahuje pět metod, které odpovídají jednotlivým testům jednotek. Každá z těchto metod je opatřena atributem [TestMethod]. Při spuštění testů jednotek je volána jakákoli metoda, která má tento atribut. Jinými slovy, každá metoda, která je zdobena atributem [TestMethod], je testem jednotek.
První test jednotek s názvem CreateContact() ověřuje, že volání CreateContact() vrátí hodnotu true, když je do metody předána platná instance třídy Contact. Test vytvoří instanci třídy Contact, zavolá metodu CreateContact() a ověří, že CreateContact() vrátí hodnotu true.
Zbývající testy ověřují, že při volání metody CreateContact() s neplatným Contact vrátí metoda false a očekávaná chybová zpráva ověření se přidá do stavu modelu. Například Test CreateContactRequiredFirstName() vytvoří instanci třídy Contact s prázdným řetězcem pro jeho vlastnost FirstName. Dále je volána metoda CreateContact() s neplatným Contact. Nakonec test ověří, že funkce CreateContact() vrací hodnotu false a že stav modelu obsahuje očekávanou chybovou zprávu ověření "Jméno je povinné".
Testy jednotek ve výpisu 1 můžete spustit výběrem možnosti nabídky Test, Spustit, Všechny testy v řešení (CTRL+R, A). Výsledky testů se zobrazí v okně Výsledky testu (viz obrázek 4).
Obrázek 04: Výsledky testů (kliknutím zobrazíte obrázek v plné velikosti)
Vytváření testů jednotek pro kontrolery
ASP.NET aplikace MVC řídí tok interakce uživatele. Při testování kontroleru chcete otestovat, jestli kontroler vrátí správný výsledek akce a zobrazí data. Můžete také otestovat, jestli kontroler komunikuje s třídami modelu očekávaným způsobem.
Například výpis 2 obsahuje dva testy jednotek pro metodu Create() kontroleru kontaktu. První test jednotek ověří, že při předání platného kontaktu metodě Create() metoda Create() přesměruje na akci Index. Jinými slovy, při předání platného contactu by metoda Create() měla vrátit RedirectToRouteResult, která představuje akci indexu.
Při testování vrstvy kontroleru nechceme testovat vrstvu služby ContactManager. Proto v metodě Initialize napodobeme vrstvu služby následujícím kódem:
_service = New Mock(Of IContactManagerService)()
V testu jednotek CreateValidContact() napodobujeme chování volání metody vrstvy služby CreateContact() pomocí následujícího řádku kódu:
_service.Expect( Function(s) s.CreateContact(contactToCreate) ).Returns(True)
Tento řádek kódu způsobí, že napodobeniny ContactManager služba vrátí hodnotu true, když je volána metoda CreateContact(). Napodobením vrstvy služby můžeme otestovat chování kontroleru, aniž bychom museli spouštět kód ve vrstvě služby.
Druhý test jednotky ověří, že akce Create() vrátí zobrazení Vytvořit, když je metodě předán neplatný kontakt. Způsobíme, že metoda vrstvy služby CreateContact() vrátí hodnotu false s následujícím řádkem kódu:
_service.Expect( Function(s) s.CreateContact(contactToCreate) ).Returns(False)
Pokud se metoda Create() chová podle očekávání, měla by vrátit zobrazení Create, když vrstva služby vrátí hodnotu false. Kontroler tak může zobrazit chybové zprávy ověření v zobrazení Vytvořit a uživatel bude mít možnost opravit neplatné vlastnosti Kontaktu.
Pokud plánujete sestavit testy jednotek pro kontrolery, musíte z akcí kontroleru vrátit explicitní názvy zobrazení. Například nevrací podobné zobrazení:
Return View()
Místo toho vraťte zobrazení takto:
Návratové zobrazení ("Vytvořit")
Pokud nejste explicitní při vrácení zobrazení ViewResult.ViewName vlastnost vrátí prázdný řetězec.
Výpis 2 – Controllers\ContactControllerTest.vb
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports Moq
Imports System.Web.Mvc
<TestClass()> _
Public Class ContactControllerTest
Private _service As Mock(Of IContactManagerService)
<TestInitialize()> _
Public Sub Initialize()
_service = New Mock(Of IContactManagerService)()
End Sub
<TestMethod()> _
Public Sub CreateValidContact()
' Arrange
Dim contactToCreate = New Contact()
_service.Expect(Function(s) s.CreateContact(contactToCreate)).Returns(True)
Dim controller = New ContactController(_service.Object)
' Act
Dim result = CType(controller.Create(contactToCreate), RedirectToRouteResult)
' Assert
Assert.AreEqual("Index", result.RouteValues("action"))
End Sub
<TestMethod()> _
Public Sub CreateInvalidContact()
' Arrange
Dim contactToCreate = New Contact()
_service.Expect(Function(s) s.CreateContact(contactToCreate)).Returns(False)
Dim controller = New ContactController(_service.Object)
' Act
Dim result = CType(controller.Create(contactToCreate), ViewResult)
' Assert
Assert.AreEqual("Create", result.ViewName)
End Sub
End Class
Souhrn
V této iteraci jsme vytvořili testy jednotek pro naši aplikaci Contact Manager. Tyto testy jednotek můžeme kdykoli spustit, abychom ověřili, že se naše aplikace stále chová očekávaným způsobem. Testy jednotek fungují jako bezpečnostní síť pro naši aplikaci, což nám v budoucnu umožňuje bezpečně upravit naši aplikaci.
Vytvořili jsme dvě sady testů jednotek. Nejprve jsme otestovali logiku ověřování vytvořením testů jednotek pro vrstvu služby. Dále jsme otestovali logiku řízení toku vytvořením testů jednotek pro vrstvu kontroleru. Při testování vrstvy služby jsme izolovali naše testy pro naši vrstvu služby od vrstvy úložiště napodobením vrstvy úložiště. Při testování vrstvy kontroleru jsme naše testy pro vrstvu kontroleru izolovali napodobením vrstvy služby.
V další iteraci upravíme aplikaci Contact Manager tak, aby podporovala skupiny kontaktů. Tuto novou funkci přidáme do naší aplikace pomocí procesu návrhu softwaru, který se nazývá vývoj řízený testy.