poniedziałek, 11 marca 2013

ASP.MVC4 Routing Testy cz1

Jakiś czas temu dostałem w swoje ręce nowiutką "Pro ASP.NET MVC4" Apress'a. Znajdują się w niej działy 13 i 14 poświęcone routingowi. Są tam zaprezentowane ciekawe testy oraz podkreślone jest ich znaczenie w całej aplikacji. Nawet jeśli pomija się testowanie innych części, test routingu jest wręcz wskazany. Schematy URL mogą stać się szybko bardzo skomplikowane i łatwo jest o nagłe, nieoczekiwane i niepożądane zachowania.

Ostatnio postanowiłem także bliżej zapoznać się z frameworkiem Nsubstitute. Jest to framework do tworzenia mock'ów, dużo bardziej intuicyjny niż prezentowany w książce Moq.

Do testowania używam frameworka xUnit oraz pluginu do VisualStudio pozwalającego na uruchamianie w nim xUnitowych testów.

Do sprawnego testowania potrzebne będą 2 metody pomocnicze:

  • metoda udająca zapytanie, odpowiedź oraz treść zapytania Http
  • metoda porównająca dane ścieżki z nazwą kontrolera, akcji oraz dodatkowych parametrów
Pierwsze 2 metody wyglądają następująco:


using System;
using System.Web;
using Xunit;
using NSubstitute;
using System.Web.Routing;
using System.Reflection;
using Routing1;

namespace UnitTests
{
    public class Tests
    {
        private HttpContextBase CreateHttpContext(string targetUrl = null,
            string httpMethod = "Get")
        {
            //create the mock request
            var mockRequest = Substitute.For<httprequestbase>();
            mockRequest.AppRelativeCurrentExecutionFilePath.Returns(targetUrl);
            mockRequest.HttpMethod.Returns(httpMethod);

            //create the mock response
            var mockResponse = Substitute.For<httpresponsebase>();
            mockResponse.ApplyAppPathModifier(Arg.Any<string>())
                .Returns(s=>s[0]);

            //create the mock context, using the request and response
            var mockContext = Substitute.For<httpcontextbase>();
            mockContext.Request.Returns(mockRequest);
            mockContext.Response.Returns(mockResponse);

            return mockContext;
        }

        private bool TestIncomingRouteResult(RouteData routeResult, 
            string controller, string action, object propertySet = null)
        {
            Func<object, bool, object> valCompare = (v1, v2) =>
            {
                return StringComparer.InvariantCultureIgnoreCase
                    .Compare(v1, v2) == 0;
            };

            bool result = valCompare(routeResult.Values["controller"], controller)
                && valCompare(routeResult.Values["action"], action);

            if (propertySet != null)
            {
                PropertyInfo[] propInfo = propertySet.GetType().GetProperties();
                foreach (var p in propInfo)
                {
                    if (!(routeResult.Values.ContainsKey(p.Name)
                        && valCompare(routeResult.Values[p.Name],
                        p.GetValue(propertySet, null))))
                    {
                        result = false;
                        break;
                    }
                }
            }
            return result;
        }
    }
}

Pierwsza metoda pozwala na przekazanie URL'a poprzez właściwość AppRelativeCurrentExecutionFilePath klasy HttpRequestBase.
Druga metoda pozwala nam przetestować samą ścieżkę.

Następnie dopisane zostały kolejne 3 metody pozwalające na sprawdzenie poprawności ścieżki, sprawdzenie błędnej ścieżki oraz metody testującej kilka przykładowych ścieżek.

private void TestRouteMatch(string url, string controller, string action,
    object routeProperties = null, string httpMethod = "GET")
{
    //Arrange
    RouteCollection routes = new RouteCollection();
    RouteConfig.RegisterRoutes(routes);

    //Act
    RouteData result
        = routes.GetRouteData(CreateHttpContext(url, httpMethod));

    //Assert
    Assert.NotNull(result);
    Assert.True(TestIncomingRouteResult(result, controller, action, routeProperties));
}
private void TestRouteFail(string url)
{
    RouteCollection routes = new RouteCollection();
    RouteConfig.RegisterRoutes(routes);

    RouteData result = routes.GetRouteData(CreateHttpContext(url));

    Assert.True(result == null || result.Route == null);
}

[Fact]
public void TestIncomingRoutes()
{
    //check for the URL that we hope to receive
    TestRouteMatch("~/Admin/Index","Admin","Index");
    //check that the values are baing obtained from the segments
    TestRouteMatch("~/One/Two", "One", "Two");

    TestRouteFail("~/Admin/Index/Segment");
    TestRouteFail("~/Admin");
}


Należy pamiętać, aby do projektu z testami dodać referencję do naszego prawdziwego projektu. W nim zawiera się kalsa RouteConfig wykorzystywana w metodach pomocniczych zawierająca metodę RegisterRoutes, gdzie domyślnie powinniśmy mieć zdefiniowane wszystkie ścieżki w naszej aplikacji.

Tak przygotowane testy mogą posłużyć do bardziej skomplikowanego testowania, którego przykłady zostaną zaprezentowane w kolejnej części.