V posledním dílu tohoto seriálu doděláme podporu pro unit testy, vytvoříme falešnou DinnerRepository třídu a napíšeme si pár dalších testů.
Navážeme na minulý díl a upravíme DinnersController tak, aby podporoval rozhraní IDinnerRepository namísto třídy DinnerRepository.
Momentálně vypadá třída DinnersController takto:
public class DinnersController : Controller
{
DinnerRepository dinnerRepository = new DinnerRepository();
Upravená verze bude vypadat následovně:
public class DinnersController : Controller
{
IDinnerRepository dinnerRepository;
public DinnersController() : this(new DinnerRepository())
{}
public DinnersController(IDinnerRepository repository)
{
dinnerRepository = repository;
}
Jak jistě vidíte, přidali jsme do třídy DinnersController dva konstruktory – první použije naši současnou implementaci třídy DinnerRepository, druhý umožňuje předat nějakou jinou třídu odvozenou od rozhraní IDinnerRepository.
Vytvoření třídy FakeDinnerRepository
Začneme vytvořením nového adresáře pojmenovaného „Fakes“ v projektu NerdDinner.Tests a rovnou do něj přidáme novou třídu FakeDinnerRepository.
Nová třída bude implementovat rozhraní IDinnerRepository, ukážu vám už rovnou finální kód:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using NerdDinner.Models;
namespace NerdDinner.Tests.Fakes
{
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 Dinner GetDinner(int id)
{
return dinnerList.SingleOrDefault(d => d.DinnerID == id);
}
public IQueryable<Dinner> FindByLocation(float latitude, float longitude)
{
return (from dinner in dinnerList
where dinner.Latitude == latitude && dinner.Longitude == longitude
select dinner).AsQueryable();
}
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("Porušení validačních pravidel");
}
}
}
}
Výborně, teď máme třídu implementující IDinnerRepository, která nepotřebuje připojení k databázi, ale pracuje jen s objekty v paměti.
Použití třídy FakedinnerRepository v unit testech
V minulém dílu jsme si napsali unit testy, které neprošly kvůli připojení k databázi, teď už to můžeme snadno napravit:
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using NerdDinner.Controllers;
using NerdDinner.Models;
using NerdDinner.Tests.Fakes;
namespace NerdDinner.Tests.Controllers
{
[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 = "Nějaká večeře",
HostedBy = "Někdo",
Address = "Nějaká adresa",
Country = "USA",
ContactPhone = "425-555-1212",
Description = "Nějaký popis",
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()
{
// Vytvoření pokusného objektu
var controller = CreateDinnersController();
// Úkon
var result = controller.Details(1);
// Rozhodnutí o výsledku testu
Assert.IsInstanceOfType(result, typeof(ViewResult));
}
[TestMethod]
public void DetailsAction_Should_Return_NotFoundView_For_BogusDinner()
{
// Vytvoření pokusného objektu
var controller = CreateDinnersController();
// Úkon
var result = controller.Details(999) as ViewResult;
// Rozhodnutí o výsledku testu
Assert.AreEqual("NotFound", result.ViewName);
}
}
}
Nyní by měly oba testy proběhnout bez problémů. Máme teď v rukou neskutečně silný nástroj pro testování DinnersControlleru – můžeme testovat jakoukoliv část aplikace používající tento controller bez přístupu k databázi, vše tak trvá jen pár sekund (ale testy si pořád musíme napsat sami).
Unit testy akční metody Edit
Teď je na řadě akční metoda Edit, nejprve její HTTP-GET verze:
[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ěřuje, zda je view vrácený objektem DinnerFormViewModel vyrenderován, pokud zažádáme o validní večeři:
[TestMethod]
public void EditAction_Should_Return_View_For_ValidDinner()
{
// Vytvoření pokusného objektu
var controller = CreateDinnersController();
// Úkon
var result = controller.Edit(1) as ViewResult;
// Rozhodnutí o výsledku testu
Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}
Tento test ale vrátí chybu. A to z poměrně jasného důvodu – dostaneme totiž výjimku null reference, když se metoda Edit pokusí přistoupit k vlastnosti User.Identity.Name pomocí metody Dinner.IsHostedBy().
Objekt User bázové třídy Controller zapouzdřuje detaily o přihlášeném uživateli. My ale testujeme DinnersController mimo webové prostředí a objektu User nejsou vůbec přiřazeny nějaké hodnoty (proto null reference). Proto si ho opět zfalšujeme.
Zfalšování vlastnosti User.Identity.Name
Za účelem vytváření „nepravých“ informací vznikly tzv. „mocking“ frameworky. Takový mocking framework nám může například dynamicky vytvářet objekt User, ze kterého pak budeme načítat uživatelské jméno.
Existuje hned několik mocking frameworků s podporou ASP.NET MVC, jejich výčet naleznete například zde. My využijeme framework nazvaný „Moq“, který stáhnete odsud.
Po stažení přidáme referenci na Moq.dll do projektu NerdDinner.Tests.
Teď nám už nic nebrání vytvořit pomocnou metodu, která přijme uživatelské jméno jako parametr a dosadí ho pomocí vlastnosti User.Identity.Name do instance DinnersControlleru:
// Nezapomeňte přidat direktivu using Moq; !
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 kódu výše využíváme instanci třídy Mock pro vytvoření falešného ControllerContext objektu (to je objekt, který ASP.NET MVC předává všem Controller třídám a vystavuje objekty jako User, Request, Response a Session). Metodou SetupGet říkáme, že „userName“, který jsme předali metodě jako parametr, má být vrácen jako HttpContext.User.Identity.Name.
Podobným způsobem můžeme zfalšovat kolik vlastností a metod se nám jen zachce, například druhé volání metody SetupGet v kódu výše vrací „true“ pro autentifikaci uživatele.
Nyní můžeme vytvořit několik testů, které pro svou funkčnost vyžadují uživatelské jméno:
[TestMethod]
public void EditAction_Should_Return_EditView_When_ValidOwner()
{
// Vytvoření pokusného objektu
var controller = CreateDinnersControllerAs("Někdo");
// Úkon
var result = controller.Edit(1) as ViewResult;
// Rozhodnutí o výsledku testu
Assert.IsInstanceOfType(result.ViewData.Model, typeof(DinnerFormViewModel));
}
[TestMethod]
public void EditAction_Should_Return_InvalidOwnerView_When_InvalidOwner()
{
// Vytvoření pokusného objektu
var controller = CreateDinnersControllerAs("Ne_vlastník");
// Úkon
var result = controller.Edit(1) as ViewResult;
// Rozhodnutí o výsledku testu
Assert.AreEqual(result.ViewName, "InvalidOwner");
}
Ještě nesmíte zapomenout přepsat v testu EditAction_Should_Return_View_For_ValidDinner volání metody „CreateDinnersController“ na novou „CreateDinnersControllerAs“, která využívá mockingu. Všechny testy už by měly proběhnout v pořádku.
Edit ve verzi HTTP-POST
Testování GET verze metody Edit máme za sebou, na závěr celého seriálu se podíváme na její POST verzi. To, co dělá tuto situaci zajímavou, je přítomnost volání UpdateModel v metodě Edit:
// POST: /Dinners/Edit/2
[AcceptVerbs(HttpVerbs.Post), Authorize]
public ActionResult Edit(int id, FormCollection formValues)
{
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.AddRuleViolations(dinner.GetRuleViolations());
return View(new DinnerFormViewModel(dinner));
}
}
Níže vidíte dva testy, které ukazují způsob, jak metodě UpdateModel přiřadit informace, které má použít. Používáme k tomu objekt FormCollection, který potřebnými daty naplníme a pak ho přiřadíme do vlastnosti ValueProvider na controlleru.
První test ověřuje, zda je při úspěšném uložení uživatel přesměrován na akční metodu „Details“. Druhý test zase zjišťuje, zda je při neplatném vstupu znovu zobrazen view a chybová zpráva:
[TestMethod]
public void EditAction_Should_Redirect_When_Update_Successful()
{
// Vytvoření pokusného objektu
var controller = CreateDinnersControllerAs("Někdo");
var formValues = new FormCollection()
{
{"Title", "blabla"},
{"Description", "Nějaký popis"}
};
controller.ValueProvider = formValues.ToValueProvider();
// Úkon
var result = controller.Edit(1, formValues) as RedirectToRouteResult;
// Rozhodnutí o výsledku testu
Assert.AreEqual("Details", result.RouteValues["Action"]);
}
[TestMethod]
public void EditAction_Should_Redisplay_With_Errors_When_Update_Fails()
{
// Vytvoření pokusného objektu
var controller = CreateDinnersControllerAs("Někdo");
var formValues = new FormCollection()
{
{"EventDate", "(tady by mělo být datum, místo toho je tu tenhle text)"}
};
controller.ValueProvider = formValues.ToValueProvider();
// Úkon
var result = controller.Edit(1, formValues) as ViewResult;
// Rozhodnutí o výsledku testu
Assert.IsNotNull(result, "Očekáváno znovuzobrazení view");
Assert.IsTrue(result.ViewData.ModelState.Count > 0, "Očekávány chyby");
}
Závěr
A máme to za sebou! Tím myslím skutečně vše, co jsem vám chtěl v tomto seriálu sdělit. Nezbývá mi než doufat, že jste se naučili mnoho nových věcí a budete se této zajímavé technologii věnovat i nadále. Těším se na viděnou u dalšího seriálu.