Unit Testing registered routes in MVC

When you have a lot of routes in a MVC application, it could be very tricky to get things right. It is always helpful to have unit testings against the registered routes. Especially when you make changes and it makes sure you will not break anything that already exists. I will show you a couple of examples how do I write unit testings for my routes.

        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

            routes.MapRoute(
                "PageRoute", // Route name
                "Page/{action}/{id}", // URL with parameters
                new { controller = "Home", id = UrlParameter.Optional } // Parameter defaults
            );

            routes.MapRoute(
                "Default", // Route name
                "{controller}/{action}/{id}", // URL with parameters
                new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
            );

            routes.MapRoute(
                "ErrorRoute", // Route name
                "{*anything}",
                new { controller = "Home", action = "Missing"} // Parameter defaults
            );

        }

In the code snippet, I have registered 3 routes. “PageRoute”, “Default” and “ErrorRoute”.

Now let’s create a unit testing project, and use Moq as the mock framework. (You can also use Rhino Mocks). Don’t forget to reference you mvc application then we are ready to write some tests.

    [TestClass]
    public class HomeControllerTest
    {
        [TestMethod]
        public void DefaultRouteTest()
        {
            var routes = new RouteCollection();
            MvcApplication.RegisterRoutes(routes);

            var context = new Mock<HttpContextBase>();
            context.Setup(p => p.Request.AppRelativeCurrentExecutionFilePath).Returns("~/");
            var routeData = routes.GetRouteData(context.Object);

            Assert.AreEqual(((Route)routeData.Route).Url, "{controller}/{action}/{id}");
            Assert.AreEqual("Home", routeData.Values["controller"], "default controller is home controller");
            Assert.AreEqual("Index", routeData.Values["action"], "default action");
        }

        [TestMethod]
        public void PageRouteTest()
        {
            var routes = new RouteCollection();
            MvcApplication.RegisterRoutes(routes);

            var context = new Mock<HttpContextBase>();
            context.Setup(p => p.Request.AppRelativeCurrentExecutionFilePath).Returns("~/Page/About");
            var routeData = routes.GetRouteData(context.Object);

            Assert.AreEqual(((Route)routeData.Route).Url, "Page/{action}/{id}");
            Assert.AreEqual("Home", routeData.Values["controller"], "default controller is home controller");
            Assert.AreEqual("About", routeData.Values["action"], "default action");
        }

        [TestMethod]
        public void ErrorRouteTest()
        {
            var routes = new RouteCollection();
            MvcApplication.RegisterRoutes(routes);

            var context = new Mock<HttpContextBase>();
            context.Setup(p => p.Request.AppRelativeCurrentExecutionFilePath).Returns("~/DDA/asd/asd/asd/WhereIsIt");
            var routeData = routes.GetRouteData(context.Object);

            Assert.AreEqual(((Route)routeData.Route).Url, "{*anything}");
            Assert.AreEqual("Home", routeData.Values["controller"], "default controller is home controller");
            Assert.AreEqual("Missing", routeData.Values["action"], "default action");

        }

    }

When writing the unit tests, you will need to mock HttpContextBase and setup the Request.AppRelativeCurrentExecutionFilePath and make it returns your targeting url. Pass the context to your route collection. You can use Assert to test what route is returned in the RouteData.

Create custom routes in MVC

Routes are core components of mvc application, they determines how the URLs are mapped to the controller and controller actions.

When you define a route in Global.asax file, you need to specify the expected layout of any matching URL, such as {controller}, {action} in the default route, default values, constraints and data tokens.

Here is an example of the default route,

routes.MapRoute(
"Default", // Route name
"{controller}/{action}/{id}", // URL with parameters
new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults
);

The first segment of the URL is mapped to the controller name, the second segment of the URL is mapped to the controller action, and the final segment of the URL is mapped to a parameter named Id.

Now let’s create a custom route and place it before the default route, There are two segments in the URL, the first is a constant value “archive”, the second is a string value for “entryDate”, the URL is mapped to Blog controller and Details method.

            routes.MapRoute(

                 "Archive",   

                 "Archive/{entryDate}",  

                 new { controller = "Archive", action = "Details"}

             );

Further more, we create a constraint for this custom route, the constraint is defined by using regular expression, so here it restricts the second segment of the URL to be a formatted string “dd-dd-dddd” with digits.

            routes.MapRoute(

                 "Archive", // Name  

                 "Archive/{entryDate}", // URL

                 new { controller = "Blog", action = "Details" }, // Defaults

                 new { entryDate = @"\d{2}-\d{2}-\d{4}" } // Constraints

             );

There is another type of route – Catch all route, with a “*” in front of the segment. A catchall route is a route that can contain any number of segments. For example, “Product/{*values}” the catchall route will match:

/Product/a

/Product/a/b

/Product/a/b/c

routes.MapRoute(

   "CatchIt", // Name

   "Product/{*values}",  // URL

   new { controller = "Product", action = "Index" } // Defaults

);

Be careful with this type of routes, because it matches the first segment and ignore the rest segments of the URL, we normally register this route after our default route.

The best way of handling the route exceptions in MVC

What is route exception?

When a URL of the incoming request doesn’t match any of the mapped routes, in this case you get a HTTP 404 error. Let users receive the default 404 ASP.NET page is not a good practice. And we are going to handle it in a better way.

The typical solution is to enforce .net framework to use custom error pages, here’s how to register ad hoc routes in asp.net,

    <customErrors defaultRedirect="~/error" mode="Remote">
      <error statusCode="404" redirect="~/error/NotFound"></error>
    </customErrors>

The trick is working just fine. There is no reason to question it from a functional perspective. But what is the problem?
Let’s take a close look at it. Imagine that when we are requesting a invalid URL, the application issues an HTTP 302 code and tells the caller it is temporarily moved to another location. Then the application redirect to the error page.

For human, you probably will not even notice it. But for Search engines, it leads the search engines to conclude the content is not missing at all. And an error page is catalogued as regular content.

The better way of dealing with the missing content is to register a catch all route, to capture any URLs sent to the application that haven’t been mapped to the existing routes. Here is the sample,

routes.MapRoute(

   "Errors", 

   "{*anything}",  

   new { controller = "Error", action = "Missing" } 

);

Of course, you will need an Error controller to handle the request and present a nice view to the users.

public class ErrorController : Controller
{
  HttpContext.Response.StatusCode = 404;
  HttpContext.Response.TrySkipIisCustomErrors = true;

  //log error
  return View();
}

TrySkipIisCustomErrors property of the response object is a new property to address a feature of the IIS 7 integrated pipeline.