Testing Routes In ASP.NET MVC

The URL routing system within ASP.NET MVC plays a very important role. Routes map incoming requests for URLs to a Controller and Action. They also are used to construct an URL to a Controller/Action. In this way, they provide a two-way mapping between URLs and controller actions.

When building routes, it may be useful to write unit tests for the routes to ensure that you’ve set up the proper mappings you intend.

ScottGu touched a bit on unit testing routes in part 2 of his series on MVC in which he covers URL Routing. In this post, we’ll go into a little more depth with testing routes.

To keep it interesting, let me show you what the final unit test looks like for testing a route. That way, if you don’t care about the details, you can skip all this discussion and just download the code.

[TestMethod]
public void RouteHasDefaultActionWhenUrlWithoutAction()
{
  RouteCollection routes = new RouteCollection();
  RouteManager.RegisterRoutes(routes);

  TestHelper.AssertRoute(routes, "~/product"
    , new { controller = "product", action = "Index" });
}

The first part of the test is simply creating a collection of routes. The second part is a call to another helper method that attempts to map a route to the specified virtual URL and compares the route data to a dictionary of name/value pairs. The dictionary is passed in as an anonymous type using a technique that my coworker Eilon Lipton wrote about on his blog.

In the first part, I call a static helper method named RouteManager.RegisterRoutes which populates a RouteCollection for me. I use this same method in Global.asax.cs like so...

public class Global : System.Web.HttpApplication
{
  protected void Application_Start(object sender, EventArgs e)
  {
    RouteManager.RegisterRoutes(RouteTable.Routes);
  }
}

This keeps all my routes in one place and easily accessible to unit tests.

Let’s take a quick look at the RegisterRoutes method so you can see which routes I am testing.

public static void RegisterRoutes(RouteCollection routes)
{
  routes.Add(new Route
  {
    Url = "blog/[year]/[month]/[day]",
    Defaults = new { controller="Blog", action = "Index"
      , id = (string)null },
    Validation = new {year=@"\d{4}", month=@"\d{2}"
      , day=@"\d{2}"},
    RouteHandler = typeof(MvcRouteHandler)
  });

  routes.Add(new Route
  {
    Url = "[controller]/[action]/[id]",
    Defaults = new { action = "Index", id = (string)null },
    RouteHandler = typeof(MvcRouteHandler)
  });

  routes.Add(new Route
  {
    Url = "[controller].mvc/[action]/[id]",
    Defaults = new { action = "Index", id = (string)null },
    RouteHandler = typeof(MvcRouteHandler)
  });

  RouteTable.Routes.Add(new Route
  {
    Url = "Default.aspx",
    Defaults = new { controller = "Home", action = "Index"
      , id = (string)null },
    RouteHandler = typeof(MvcRouteHandler)
  });
}

Looks like your standard routes. I threw one in there with an extension and a another one that looks like one you might use with a blog.

Next, I’ll show you how I would write a test the long way using a mock framework.

[TestMethod]
public void RouteTestTheLooooongWay()
{
  RouteCollection routes = new RouteCollection();
  RouteManager.RegisterRoutes(routes);

  MockRepository mocks = new MockRepository();
  IHttpContext httpContext;

  using (mocks.Record())
  {
    httpContext = mocks.DynamicMock<IHttpContext>();
    IHttpRequest request = mocks.DynamicMock<IHttpRequest>();
    SetupResult.For(httpContext.Request).Return(request);
    mocks.Replay(httpContext);

    SetupResult.For(httpContext.Request.AppRelativeCurrentExecutionFilePath)
      .Return("~/product/list");
    SetupResult.For(httpContext.Request.PathInfo).Return(string.Empty);
  }

  using (mocks.Playback())
  {
    RouteData routeData = routes.GetRouteData(httpContext);
    Assert.IsNotNull(routeData, "Should have found the route");
    Assert.AreEqual("product", routeData.Values["Controller"]
      , "Expected a different controller");
    Assert.AreEqual("list", routeData.Values["action"]
      , "Expected a different action");
  }
}

Yikes! While it may seem like a lot of code, it’s pretty straightforward, assuming you understand the general pattern for using a mock framework.

However, we can shorten a lot of this code by using an extension method I wrote in a previous post. I actually wrote an overload that makes it easier to mock a request for a specific URL.

That gets us further, but we can do so much more. Here is the code I wrote for my AssertRoute method.

public static void AssertRoute(RouteCollection routes
    , string url, object expectations)
{
  MockRepository mocks = new MockRepository();
  IHttpContext httpContext;

  using (mocks.Record())
  {
    httpContext = mocks.DynamicIHttpContext(url);
  }

  using (mocks.Playback())
  {
    RouteData routeData = routes.GetRouteData(httpContext);
    Assert.IsNotNull(routeData, "Should have found the route");

    foreach (PropertyValue property in GetProperties(expectations))
    {
      Assert.IsTrue(string.Equals((string)property.Value
        , (string)routeData.Values[property.Name]
        , StringComparison.InvariantCultureIgnoreCase)
        , string.Format("Did not expect '{0}' for '{1}'."
          , property.Value, property.Name));
    }
  }
}

This code makes use of the GetProperties method I lifted from Eilon’s blog post, Using C# 3.0 Anonymous types as Dictionaries.

The expectations passed to this method are the name/value pairs you expect to see in the RouteData.Values dictionary.

NOTE: In the downloadable code, I added an overload so you could specify the expected route handler. Most of the time this is going to be MvcRouteHandler.

I hope you find this useful. The code (along with other unit test examples) are in  solution ready for download.

UPDATE: I just updated the solution (12/17 6:00 PM PST) with a fix for the whole Default.aspx issue. It was a dumb bug on my part when I set up the routes.

[ad] Free Bug Tracking & Project Management Software Axosoft’s OnTime 2007 allows software development teams to collaborate on software projects by tracking everything from defects to enhancements to helpdesk incidents in one easy-to-use database driven by an intuitive Windows, Web or VS.NET Integrated UI. Get a Free Single-User License ($200 Value!)

What others have said

Requesting Gravatar... Adam Tybor Dec 17, 2007 6:50 AM
# re: Testing Routes In ASP.NET MVC
Nice post Phil, looks very familiar :)
Requesting Gravatar... Jason Haley Dec 17, 2007 8:36 AM
# Interesting Finds: December 17, 2007
Requesting Gravatar... Haacked Dec 17, 2007 2:09 PM
# re: Testing Routes In ASP.NET MVC
Someone wrote me an email and pointed out I haven't addressed URLs with query string parameters. I'll take a look at this soon.
Requesting Gravatar... Haacked Dec 17, 2007 6:01 PM
# re: Testing Routes In ASP.NET MVC
Actually, just found out that query string parameters are not part of routing. It's the Controller action invocation that matches query string parameters to the action method's parameters.

So the part that is the route is everything up to the query string question mark. Make sense?
Requesting Gravatar... Christopher Steen Dec 18, 2007 4:57 AM
# Link Listing - December 17, 2007
Sharepoint SLEEPLESS ROADSHOW – The Ultimate Office Dev Weekend [Via: Public Sector DPE Team ] WPF ...
Requesting Gravatar... Haacked Dec 18, 2007 10:03 PM
# re: Testing Routes In ASP.NET MVC
Updated my samples to better handle querystrings. Thanks David!
Requesting Gravatar... DotNetKicks.com Dec 19, 2007 1:43 AM
# Testing Routes in ASP.NET MVC
You've been kicked (a good thing) - Trackback from DotNetKicks.com
Requesting Gravatar... Community Blogs Dec 19, 2007 2:32 AM
# First Thoughts On MVC.NET
Finally some time for .net. Since Scott and Phil started writing about is I wanted to read and try out
Requesting Gravatar... Vijay Santhanam Jan 02, 2008 7:01 AM
# re: Testing Routes In ASP.NET MVC
I found this test approach mightily useful for testing routes and I'll keep this on my tool-belt for my next MVC project.
Requesting Gravatar... My World Jan 03, 2008 9:38 PM
# Stop Celebrating, Start Learning...
After a great start to the new year, I've taken the first three days back at work as PD days to catch
Requesting Gravatar... Lance Fisher Jan 06, 2008 8:10 PM
# re: Testing Routes In ASP.NET MVC
Hi Phil, thanks for this I've been able to start testing routes like this, and I really like it.

I'm wondering why in your SetMockedRequestUrl() method you disallow using domains in the urls. You throw an error if the url does not start with "~/". Is this a limitation of the routing engine? I am trying to set up a site that has routing similar Amazon S3. That is, both ~/mysite/blog and mysite.com/blog will route to the same place. The routing should allow for any number of mysite's to be created. For development, I've added several entries to my hosts file to route a couple different domains to my app.

Thanks for any tips.
Requesting Gravatar... Community Blogs Feb 04, 2008 10:13 AM
# ASP.NET MVC and User Group Fun
I did a couple user group presentations this month on the new ASP.NET MVC framework. This post is a follow

What do you have to say?

(will show your gravatar)
Please add 4 and 4 and type the answer here: