Michael Whelan

behaviour driven blog

Black-Box Testing ASP.Net: Extending Strongly Typed Navigation

In a previous post in this series, on reducing the use of magic strings, I showed a helper class for creating strongly typed navigation. This lets you derive a URL from a strongly typed controller action by looking up the route in the route table and returning you the same computed URL your application recognises.

Here is a test that illustrates the behaviour. This is a standard situation where the URL simply contains the controller and the action, as well as the ID as a route argument. RouteConfig.RegisterRoutes() is the method in the application that intialises its route table.

[Test]
public void MvcUrlHelper_should_return_correct_route_for_controller_action()
{
    var routes = RouteConfig.RegisterRoutes(new RouteCollection());
    var sut = new MvcUrlHelper(routes);

    sut.GetRelativeUrlFor<StudentController>(x => x.Details(1))
        .Should().Be("/Student/Details/1");
}

Recently, I've had a couple of reasons to extend this class. Firstly, I've been testing applications that use Areas. Secondly, I've needed to be able to pass in additional route values.

I will start off with the final class and then discuss the additional behaviour:

public class MvcUrlHelper
{
    private readonly RouteCollection routeCollection;

    public MvcUrlHelper(RouteCollection routeCollection)
    {
        this.routeCollection = routeCollection;
    }

    public string GetRelativeUrlFor<TController>(Expression<Action<TController>> action, IDictionary<string, object> routeValues = null)
        where TController : Controller
    {
        var requestContext = new RequestContext(FakeHttpContext.Root(), new RouteData());

        // Get controller and action values
        var actionRouteValues = Microsoft.Web.Mvc.Internal.ExpressionHelper.GetRouteValuesFromExpression(action);

        var area = GetArea(typeof(TController));
        if (!string.IsNullOrEmpty(area))
        {
            actionRouteValues.Add("Area", area);
        }

        if (routeValues != null)
        {
            foreach (var v in routeValues) actionRouteValues[v.Key] = v.Value;
        }

        var urlHelper = new UrlHelper(requestContext, this.routeCollection);
        var relativeUrl = urlHelper.RouteUrl(new RouteValueDictionary(actionRouteValues));

        return relativeUrl;
    }

    private static string GetArea(Type controllerType)
    {
        var routeAreaAttributes = controllerType.GetCustomAttributes(typeof(RouteAreaAttribute), true);
        if (routeAreaAttributes.Length > 0)
        {
            var routeArea = (RouteAreaAttribute)(routeAreaAttributes[0]);
            return routeArea.AreaName;
        }

        var nameSpace = controllerType.Namespace;
        if (nameSpace == null)
        {
            return string.Empty;
        }

        const string AreasStartSearchString = "Areas.";
        var areasIndexOf = nameSpace.IndexOf(AreasStartSearchString, StringComparison.Ordinal);
        if (areasIndexOf < 0)
        {
            return string.Empty;
        }

        var areaStart = areasIndexOf + AreasStartSearchString.Length;
        var areaString = nameSpace.Substring(areaStart);
        if (areaString.Contains("."))
        {
            areaString = areaString.Remove(areaString.IndexOf(".", StringComparison.Ordinal));
        }

        return areaString;
    }
}

Areas

The MVC5 Futures ExpressionHelper class does not return the area in the URL (unless you use its Area attribute). Here is the test that illustrates the behaviour I want, where University is the Area and Student the controller.

[TestMethod]
public void should_return_area_in_url()
{
    var routes = RouteConfig.RegisterRoutes(new RouteCollection());
    var sut = new MvcUrlHelper(routes);
    var result = sut.GetRelativeUrlFor<StudentController>(c => c.Create());
    result.ShouldBe("/University/Student/Create");
}

The GetArea method tries to find area information by interrogating the controller type. Firstly, it looks for a RouteAreaAttribute on the class. Secondly, it looks at the namespace to see if it is in the standard Areas namespace, and extracts the Area from the namespace if it is. If this method returns an area then it is added to the actionRouteValues RouteValueDictionary.

Additional Route Values

Sometimes, you must provide additional route values that the UrlHelper class requires to construct a URL. This test shows the adding of an application route value, with a value of Books, which is used in the URL.

[TestMethod]
public void should_return_application_in_url()
{
    var sut = new MvcUrlHelper(new RouteRegistrator().RegisterRoutes());
    var application = new Dictionary<string, object> { { "application", "Books" } };
    var result = sut.GetRelativeUrlFor<CollectionsController>(c => c.Details(23), application);
    Assert.AreEqual("/Editorial/Applications/Books/Collections/Details?collectionId=23", result);
}

These additional values are used by UrlHelper in the creation of the full URL to make the test pass.

About Michael Whelan

Michael Whelan is a Technical Lead with over 20 years’ experience in building (and testing!) applications on the Microsoft stack. He is passionate about applying agile development practices, such as BDD and continuous delivery, to agile processes. These days his primary focus is ASP.Net MVC Core and Azure. He contributes to a number of open source frameworks through TestStack.

comments powered by Disqus
Google

Google