wtorek, 17 sierpnia 2010

ASP.NET MVC 2 cz. 8 Metodologia TDD

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