TDD Test-driven development - jedna z zwinnych metodyk (Agile) wytwarzania oprogramowania. Całą koncepcję TDD możemy przedstawić na diagramie:
Tak więc schemat jest prosty:
1. Programista chce dodać nową funkcję - pisze więc wpierw test
2. Wywołuje test który powinien zakończyć się niepowodzeniem
3. Programista implementuje żądaną funkcjonalność
4. Test w tym momencie powinien się powieść
5. Refaktoryzacja kodu
Jak to przekłada się na nasz projekt MVC?
Na początek musimy dysponować odpowiednimi narzędziami do testowania. Użyjemy dwóch popularnych narzędzi:
1. NUnit - strona domowa
2. Moq - strona domowa
Po pobraniu i zainstalowaniu tych narzędzi w naszej solucji tworzymy nowy projekt typu Class Library.
Teraz należy dodać odpowiednie referencje:
nunit.framework
system.web
system.web.Abstractions
system.web.Routing
system.web.Mvc
Moq.dll - z lokalizacji w której znajduje się zainstalowany Moq
Na koniec referencję do projektu Mvc oraz do innych wykorzystywanych projektów (np. Domain Model itp.)
Parę słów jeszcze o Mokowaniu:
Moq pozwala na tworzenie obiektów o dużej ilości zależności jak np. Response, Request itp. Przykłady zobaczymy w dalszej części.
Schemat testów najlepiej przedstawić jako model A/A/A czyli arrange/act/assert
arrange - tworzymy instancje potrzebnych klas np. kontrolera.
act - wywołujemy jakąś akcję, przekazujemy jej żądane parametry i gromadzimy wyniki
assert - upewniamy się że zwrócone rezultaty są zgodne z oczekiwanymi
Najpierw zobaczymy najprostsze przykłady testów:
1. Test na to czy kontroler zwraca poprawny widok:
A więc zaczynamy. W naszej aplikacji chcemy mieć możliwość wyświetlania produktów i szczegółowych informacji o nich. Piszemy więc test:
[TestFixture]
public class ProductControllerTest
{
[Test]
public void Can_Display_Details_View()
{
//Arrange
ProductController productController = new ProductController();
//Act
var result = (ViewResult)productController.Details(10);
//Assert
Assert.AreEqual("Details", result.ViewName);
}
}
Uruchamiamy NUnit i otrzymujemy wynik negatywny. Czas więc na implementację kontrolera i właściwej metody:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace Projekt.Controllers
{
public class ProductController : Controller
{
public ActionResult Index()
{
return View();
}
public ActionResult Details(int id)
{
return View("Details");
}
}
}
Po ponownym uruchomieniu testu powinniśmy otrzymać wynik pozytywny.
2. Testowanie zwracanych danych przez kontroler (ViewData)
Kolejny bardzo prosty typ testu to sprawdzenie czy to co przenosi w sobie ViewDate jest tym co oczekujemy.
Mamy więc sytuację: Do metody Details przekazujemy id = 10 a w odpowiedzi chcemy otrzymać informację że jest to komputer:
[Test]
public void Accion_Returns_Corret_Model()
{
ProductController productController = new ProductController();
var result = (Product)((ViewResult)productController.Details(12)).ViewData.Model;
Assert.AreEqual("komputer", result.Name);
}
Po uruchomieniu NUnit otrzymamy błąd. Przechodzimy więc do implementacji akcji:
public ActionResult Details(int id)
{
Product p = new Product
{
Id = 12,
Name = "komputer"
};
return View("Details", p);
}
3. Testowanie różnych typów zwracanych przez kontroler
Akacja może oczywiście zwrócić nie tylko widok ale również np. przekierowanie do innej strony. Zobaczmy to w praktyce:
[Test]
public void Accion_Redirect_ToIndex_When_Bad_Id()
{
ProductController productController = new ProductController();
var result = (RedirectToRouteResult)productController.Details(-5);
Assert.AreEqual("Index", result.RouteValues["action"].ToString());
}
Test oczywiście się nie powiedzie więc implementujemy rozwiązanie:
public ActionResult Details(int id)
{
if (id < 0)
{
return RedirectToAction("Index");
}
Product p = new Product
{
Id = 12,
Name = "komputer"
};
return View("Details", p);
}
Przedstawione przykładu pokazują jedynie najprostsze z możliwych testów jakie można wykonać. Bardziej złożone testy wymagają mokowania. Jest to wymagane np. w sytuacjach gdy korzystamy z Dependency Injection czy złożonych obiektów.
Kilka bardziej skomplikowanych przypadków testowych:
1. Testowanie tras routingu:
Stworzymy najpierw klasę z metodą pomocniczą, która dostarczy testowy obiekt klasy HttpContextBase:
public class TestHelpers
{
public static Mock<HttpContextBase> MakeMockHttpContext(string url)
{
var mockHttpContext = new Mock<HttpContextBase>();
var mockRequest = new Mock<HttpRequestBase>(); //Request
mockHttpContext.Setup(x => x.Request).Returns(mockRequest.Object);
mockRequest.Setup(x => x.AppRelativeCurrentExecutionFilePath).Returns(url);
var mockResponse = new Mock<HttpResponseBase>();//Response
mockHttpContext.Setup(x => x.Response).Returns(mockResponse.Object);
mockResponse.Setup(x => x.ApplyAppPathModifier(It.IsAny<string>()))
.Returns<string>(x => x);
return mockHttpContext;
}
}
Następnie implementujemy właściwy test:
[TestFixture]
public class RoutingTests
{
[Test]
public void ForwardSlashGoesToHomeIndex()
{
RouteCollection routeConfig = new RouteCollection();
MvcApplication.RegisterRoutes(routeConfig);
var mockHttpContext = TestHelpers.MakeMockHttpContext("~/");
RouteData routeData = routeConfig.GetRouteData(mockHttpContext.Object);
Assert.IsNotNull(routeData, "RouteData - null!!");
Assert.IsNotNull(routeData.Route, "Nie znaleziono pasującej trasy");
Assert.AreEqual("Home", routeData.Values["controller"], "Zły kontroler");
Assert.AreEqual("Index", routeData.Values["action"], "Zła akcja");
}
}
2. Testowanie kontrolerów
Widzieliśmy wcześniej jak testować:
- poprawność zwracanych widoków
- dane przesyłane przez ViewData
- testowaliśmy przekierowania
Teraz coś trudniejszego: w akcji wykorzystamy inne obiekty jak Request, Response, Cookie:
public ViewResult Homepage()
{
if (Request.Cookies["HasVisitedBefore"] == null)
{
ViewData["IsFirstVisit"] = true;
Response.Cookies.Add(new HttpCookie("HasVisitedBefore", bool.TrueString));
}
else
ViewData["IsFirstVisit"] = false;
return View();
}
Dla potrzeb testu stworzymy klasę pomocniczą która ustawi odpowiednie wartości potrzebnych nam obiektów:
//źródło: Apress Pro ASP.NET MVC 2 Framework
public class ContextMocks
{
public Moq.Mock<HttpContextBase> HttpContext { get; private set; }
public Moq.Mock<HttpRequestBase> Request { get; private set; }
public Moq.Mock<HttpResponseBase> Response { get; private set; }
public RouteData RouteData { get; private set; }
public ContextMocks(Controller onController)
{
HttpContext = new Moq.Mock<HttpContextBase>();
Request = new Moq.Mock<HttpRequestBase>();
Response = new Moq.Mock<HttpResponseBase>();
HttpContext.Setup(x => x.Request).Returns(Request.Object);
HttpContext.Setup(x => x.Response).Returns(Response.Object);
HttpContext.Setup(x => x.Session).Returns(new FakeSessionState());
Request.Setup(x => x.Cookies).Returns(new HttpCookieCollection());
Response.Setup(x => x.Cookies).Returns(new HttpCookieCollection());
Request.Setup(x => x.QueryString).Returns(new NameValueCollection());
Request.Setup(x => x.Form).Returns(new NameValueCollection());
RequestContext rc = new RequestContext(HttpContext.Object, new RouteData());
onController.ControllerContext = new ControllerContext(rc, onController);
}
private class FakeSessionState : HttpSessionStateBase
{
Dictionary<string, object> items = new Dictionary<string, object>();
public override object this[string name]
{
get { return items.ContainsKey(name) ? items[name] : null; }
set { items[name] = value; }
}
}
Teraz nasz test możemy zapisać jako:
[Test]
public void Homepage_Recognizes_New_Visitor_And_Sets_Cookie()
{
var controller = new SimpleController();
var mocks = new ProjektTests.TestHelpers.ContextMocks(controller);
ViewResult result = controller.Homepage();
Assert.IsEmpty(result.ViewName);
Assert.IsTrue((bool)result.ViewData["IsFirstVisit"]);
Assert.AreEqual(1, controller.Response.Cookies.Count);
Assert.AreEqual(bool.TrueString,
controller.Response.Cookies["HasVisitedBefore"].Value);
}
Tak w skrócie przedstawia się sprawa testowania metod. Pamiętajcie najpierw test - potem implementacja.
Brak komentarzy:
Prześlij komentarz