ASP.NET MVC Tip #13 – Unit Test Your Custom Routes

In this tip, I demonstrate how you can create unit tests for the routes in your ASP.NET MVC applications. I show how to test whether a URL is being mapped to the right controller, controller action, and action parameters.

If you are being virtuous about test-driven development when building an ASP.NET MVC application, then you should write unit tests for everything. Write the unit test first then write the code that satisfies the test. Repeat, repeat, repeat, ad nauseam.

Routes are a very important part of an MVC application. A route determines how a URL is mapped to a particular controller and controller action. Since routes are such an important part of an MVC application, you need to write unit tests for your routes. In this tip, I show you how to write unit tests for routes by faking the HTTP Context.

Creating a Route Table

You create the routes for an MVC application in the Global.asax file. In other words, they are defined in the GlobalApplication class. The default Global.asax file is contained in Listing 1.

Listing 1 -- Global.asax (VB.NET)

Public Class GlobalApplication
    Inherits System.Web.HttpApplication
 
    Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}")
 
        ' MapRoute takes the following parameters, in order:
        ' (1) Route name
        ' (2) URL with parameters
        ' (3) Parameter defaults
        routes.MapRoute( _
            "Default", _
            "{controller}/{action}/{id}", _
            New With {.controller = "Home", .action = "Index", .id = ""} _
        )
 
    End Sub
 
    Sub Application_Start()
        RegisterRoutes(RouteTable.Routes)
    End Sub
End Class

Listing 1 -- Global.asax (C#)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
 
namespace DefaultOne
{
    public class GlobalApplication : System.Web.HttpApplication
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            routes.MapRoute(
                "Default",                                              // Route name
                "{controller}/{action}/{id}",                           // URL with parameters
                new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
            );
 
        }
 
        protected void Application_Start()
        {
            RegisterRoutes(RouteTable.Routes);
        }
    }
}

By default, you get one route named, appropriately, the Default route. This route takes a URL and maps the different segments of the URL to a particular controller, controller action, and parameter passed to the action like this:

/Customer/Details/23

· Controller = Customer

· Action = Details

· Id = 24

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.

The Default route includes defaults. If you don’t specify a controller, the Home controller is used. If you don’t specify an action, the Index action is called. If you don’t specify an Id, an empty string is passed. Therefore, the following URL is interpreted like this:

/

· Controller = Home

· Action = Index

· Id = “”

For many types of MVC applications, this default route is the only one that you will ever need. However, you do have the option of creating custom routes. For example, the Global.asax file in Listing 2 includes two custom routes.

Listing 2 – Global.asax (VB.NET)

Public Class GlobalApplication
    Inherits System.Web.HttpApplication
 
    Shared Sub RegisterRoutes(ByVal routes As RouteCollection)
        routes.IgnoreRoute("{resource}.axd/{*pathInfo}")
 
 
        ' Route for blog archive
        routes.MapRoute("Archive", _
                        "Archive/{entryDate}", _
                        New With {Key .controller = "Blog", Key .action = "Details"}, _
                        New With {Key .entryDate = "\d{2}-\d{2}-\d{4}"})
 
        ' Default route
        routes.MapRoute("Default", _
                        "{controller}/{action}/{id}", _
                        New With {Key .controller = "Home", Key .action = "Index", Key .id = ""})
 
        ' Catch all route
        routes.MapRoute("CatchIt", _
                        "Product/{*values}", _
                        New With {Key .controller = "Product", Key .action = "Index"})
 
    End Sub
 
    Sub Application_Start()
        RegisterRoutes(RouteTable.Routes)
    End Sub
End Class

Listing 2 – Global.asax (C#)

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
 
namespace Tip13
{
    public class GlobalApplication : System.Web.HttpApplication
    {
        public static void RegisterRoutes(RouteCollection routes)
        {
            routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
 
            // Route for blog archive
            routes.MapRoute(
                 "Archive", // Name  
                 "Archive/{entryDate}", // URL
                 new { controller = "Blog", action = "Details" }, // Defaults
                 new { entryDate = @"\d{2}-\d{2}-\d{4}" } // Constraints
             );
 
            // Default route
            routes.MapRoute(
                "Default", // Name
                "{controller}/{action}/{id}",  // URL 
                new { controller = "Home", action = "Index", id = "" }  // Defaults
            );
 
            // Catch all route
            routes.MapRoute(
               "CatchIt", // Name
               "Product/{*values}",  // URL
               new { controller = "Product", action = "Index" } // Defaults
            );
 
 
        }
 
        protected void Application_Start()
        {
            RegisterRoutes(RouteTable.Routes);
        }
    }
}

The modified Global.asax in Listing 2 contains a new route named Archive that handles requests for blog entries that look like this:

/archive/12-25-1966

This custom route will map this URL to a controller named Blog and call the Details() action. The date is passed to the Details() action as a parameter named entryDate.

The Global.asax also defines a catchall route. A catchall route is a route that can contain any number of segments. For example, the catchall route will match:

/Product/a

/Product/a/b

/Product/a/b/c

And so on with any number of segments.

Unit Testing Custom Routes

So how do you test your custom routes? I couldn’t figure out how to do it until I saw a sample in the MVC unit tests included with xUnit (http://www.CodePlex.com/xUnit). In order to test custom routes, you need to fake your HTTP Context.

In the previous tip, I showed you how to use fake context objects when unit testing ASP.NET intrinsics such as session state, form parameters, and user identities and roles. If you haven’t read this blog entry, please navigate to the following page:

http://weblogs.asp.net/stephenwalther/archive/2008/06/30/asp-net-mvc-tip-12-faking-the-controller-context.aspx

After looking at the xUnit sample, I modified my fake context objects so that they can be used when unit testing routes. The unit tests in Listing 3 demonstrates how to test the custom routes contained in the Global.asax file in Listing 2.

Listing 3 – RouteTests.vb (VB.NET)

Imports System
Imports System.Text
Imports System.Collections.Generic
Imports Microsoft.VisualStudio.TestTools.UnitTesting
Imports System.Web.Routing
Imports MvcFakes
Imports Tip13
 
<TestClass()> Public Class RouteTests
 
    <TestMethod()> _
    Public Sub TestDefaultRoute()
        ' Arrange
        Dim routes = New RouteCollection()
        GlobalApplication.RegisterRoutes(routes)
 
        ' Act
        Dim context = New FakeHttpContext("~/")
        Dim routeData = routes.GetRouteData(context)
 
        ' Assert
        Assert.AreEqual("Home", routeData.Values("controller"), "Default controller is HomeController")
        Assert.AreEqual("Index", routeData.Values("action"), "Default action is Index")
        Assert.AreEqual(String.Empty, routeData.Values("id"), "Default Id is empty string")
    End Sub
 
 
    <TestMethod()> _
    Public Sub TestGoodArchiveRoute()
        ' Arrange
        Dim routes = New RouteCollection()
        GlobalApplication.RegisterRoutes(routes)
 
        ' Act
        Dim context = New FakeHttpContext("~/Archive/12-25-1966")
        Dim routeData = routes.GetRouteData(context)
 
        ' Assert
        Assert.AreEqual("Blog", routeData.Values("controller"), "Controller is Blog")
        Assert.AreEqual("Details", routeData.Values("action"), "Action is Details")
        Assert.AreEqual("12-25-1966", routeData.Values("entryDate"), "EntryDate is date passed")
 
    End Sub
 
    <TestMethod()> _
    Public Sub TestBadArchiveRoute()
        ' Arrange
        Dim routes = New RouteCollection()
        GlobalApplication.RegisterRoutes(routes)
 
        ' Act
        Dim context = New FakeHttpContext("~/Archive/something")
        Dim routeData = routes.GetRouteData(context)
 
        ' Assert
        Assert.AreNotEqual("Blog", routeData.Values("controller"), "Controller is not Blog")
    End Sub
 
 
    <TestMethod()> _
    Public Sub TestCatchAllRoute()
        ' Arrange
        Dim routes = New RouteCollection()
        GlobalApplication.RegisterRoutes(routes)
 
        ' Act
        Dim context = New FakeHttpContext("~/Product/a/b/c/d")
        Dim routeData = routes.GetRouteData(context)
 
        ' Assert
        Assert.AreEqual("a/b/c/d", routeData.Values("values"), "Got catchall values")
    End Sub
 
 
End Class

Listing 3 – RouteTests.cs (C#)

using System;
using System.Web.Routing;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcFakes;
using Tip13;
 
namespace Tip13Tests.Routes
{
    [TestClass]
    public class RoutesTest
    {
        [TestMethod]
        public void TestDefaultRoute()
        {
            // Arrange
            var routes = new RouteCollection();
            GlobalApplication.RegisterRoutes(routes);
 
            // Act
            var context = new FakeHttpContext("~/");
            var routeData = routes.GetRouteData(context);
 
            // Assert
            Assert.AreEqual("Home", routeData.Values["controller"], "Default controller is HomeController");
            Assert.AreEqual("Index", routeData.Values["action"], "Default action is Index");
            Assert.AreEqual(String.Empty, routeData.Values["id"], "Default Id is empty string");
        }
 
        [TestMethod]
        public void TestGoodArchiveRoute()
        {
            // Arrange
            var routes = new RouteCollection();
            GlobalApplication.RegisterRoutes(routes);
 
            // Act
            var context = new FakeHttpContext("~/Archive/12-25-1966");
            var routeData = routes.GetRouteData(context);
 
            // Assert
            Assert.AreEqual("Blog", routeData.Values["controller"], "Controller is Blog");
            Assert.AreEqual("Details", routeData.Values["action"], "Action is Details");
            Assert.AreEqual("12-25-1966", routeData.Values["entryDate"], "EntryDate is date passed");
 
        }
 
        [TestMethod]
        public void TestBadArchiveRoute()
        {
            // Arrange
            var routes = new RouteCollection();
            GlobalApplication.RegisterRoutes(routes);
 
            // Act
            var context = new FakeHttpContext("~/Archive/something");
            var routeData = routes.GetRouteData(context);
 
            // Assert
            Assert.AreNotEqual("Blog", routeData.Values["controller"], "Controller is not Blog");
        }
 
        [TestMethod]
        public void TestCatchAllRoute()
        {
            // Arrange
            var routes = new RouteCollection();
            GlobalApplication.RegisterRoutes(routes);
 
            // Act
            var context = new FakeHttpContext("~/Product/a/b/c/d");
            var routeData = routes.GetRouteData(context);
 
            // Assert
            Assert.AreEqual("a/b/c/d", routeData.Values["values"], "Got catchall values");
        }
    }
}

These are Visual Studio Test (MS Test) unit tests. You could, of course, use a different testing framework such as NUnit or xUnit. Here is how the unit tests work.

First, a new route collection is created and passed to the RegisterRoutes() method defined in the Global.asax file. The Global.asax file corresponds to a class named the GlobalApplication class.

Next, a fake HTTP Context is created that contains the URL being tested. For example, in the first test, the URL ~/Archive/12-25-1966 is passed to the constructor for the fake HTTP Context object. The fake HTTP Context object is a modified version of the fake MVC objects that I created in Tip #12. These fake objects are included in the MvcFakes project included with the download at the end of this blog entry.

Next, the GetRouteData() method is called with the fake context and route data is returned. The route data represents the interpreted outcome of passing a URL to the application’s route table. In other words, the route data is what you get after the URL is compared against the routes in the route table.

Finally, the test asserts that certain values are contained in the route data. In the case of the first test, the controller name, controller action, and Id value are verified. According to this test, the empty URL ~/ should get mapped to a controller named Home, an action named Index, and an Id with the value String.Empty.

The second test checks that a request that looks like ~/Archive/12-25-1966 gets mapped to a controller named Blog, an action named Details, and that a parameter named entryDate is created.

The third test checks that a request that looks like ~/Archive/something does not get mapped to the Blog controller. Since this URL does not contain a proper entry date, it should not get handled by the Blog controller.

The final test verifies that the catch-all route works correctly. This test checks that the URL ~/Product/a/b/c/d gets parsed so that the values parameter equals a/b/c/d. In other words, it checks the catch-all part of the catch-all route.

Summary

In this tip, I’ve shown you an easy way to test your custom ASP.NET MVC routes. I recommend that you unit test your routes whenever you modify the default route defined in the Global.asax file.

You can download the code for this tip, including the MvcFakes project, by clicking the following URL. The code is included in both VB.NET and C# versions.

Download the Code

6 Comments

Comments have been disabled for this content.