Testing Routes In ASP.NET MVC

The ASP.NET Routing engine used by 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();
  GlobalApplication.RegisterRoutes(routes);

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

The first part of the test simply populates a collection with routes from your Global application class defined in Global.asax.cs. 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.

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.IgnoreRoute("{resource}.axd/{*pathInfo}");

    routes.MapRoute("blog-route", "blog/{year}/{month}/{day}",
        new { controller = "Blog", action = "Index", id = "" },
        new { year = @"\d{4}", month = @"\d{2}", day = @"\d{2}" }
    );

    routes.MapRoute(
        "Default",
        "{controller}/{action}/{id}",
        new { controller = "Home", action = "Index", id = "" }
    );
}

Looks like your standard set of routes, no? I threw one in there that looks like one you might use with a blog engine.

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

[TestMethod]
public void CanMapNormalControllerActionRoute() {
    RouteCollection routes = new RouteCollection();
    GlobalApplication.RegisterRoutes(routes);

    var httpContextMock = new Mock<HttpContextBase>();
    httpContextMock.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath)
        .Returns("~/product/list");

    RouteData routeData = routes.GetRouteData(httpContextMock.Object);
    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");
}

Ok, that isn’t all that bad. It may seem like a lot of code, but 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) 
{
    var httpContextMock = new Mock<HttpContextBase>();
    httpContextMock.Setup(c => c.Request.AppRelativeCurrentExecutionFilePath)
        .Returns(url);

    RouteData routeData = routes.GetRouteData(httpContextMock.Object);
    Assert.IsNotNull(routeData, "Should have found the route");

    foreach (PropertyValue property in GetProperties(expectations)) {
        Assert.IsTrue(string.Equals(property.Value.ToString(), 
            routeData.Values[property.Name].ToString(), 
            StringComparison.OrdinalIgnoreCase)
            , string.Format("Expected '{0}', not '{1}' for '{2}'.", 
            property.Value, routeData.Values[property.Name], 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.

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

UPDATE: I updated the blog post and solution (7/27 1:38 PM PST) to account for all the changes to routing made since I wrote this post originally.

UPDATE AGAIN: Updated the post to use MoQ 3.0 and cleaned up the code a slight bit.

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
Requesting Gravatar... Travis Jul 17, 2008 9:45 AM
# re: Testing Routes In ASP.NET MVC
I hope someone reads this.. but where are these interfaces defined? I cannot find them anywhere (well my project cant find them referenced anywhere):

IHttpContext
IHttpRequest
IHttpResponse

Specifically when used here in MvcMockHelpers.cs:

IHttpContext context = mocks.DynamicMock();
IHttpRequest request = mocks.DynamicMock();
IHttpResponse response = mocks.DynamicMock();

I am using .NET 3.5, Asp.Net Preview 4.
Requesting Gravatar... Art Jan 18, 2010 7:14 PM
# re: Testing Routes In ASP.NET MVC
Hi Phil,

The TddDemo.zip appears to be corrupted. I cannot open it using anu of my compression utilities (Windows Explorer, WinZip, 7-Zip). Any chance you have a copy you could refresh?

Thanks!
Requesting Gravatar... Paul Wallace Jan 19, 2010 1:08 AM
# re: Testing Routes In ASP.NET MVC
Same here, The TddDemo.zip appears to be corrupted.
Requesting Gravatar... BjartN Feb 11, 2010 7:24 AM
# re: Testing Routes In ASP.NET MVC
File still corrupt..
Requesting Gravatar... Sayed Ibrahim Hashimi Mar 14, 2010 11:20 AM
# re: Testing Routes In ASP.NET MVC
Looks like the .zip file is still invalid. Just downloaded it, and bang!
Requesting Gravatar... Scott McNeany Jun 02, 2010 1:44 PM
# re: Testing Routes In ASP.NET MVC
This is very similar to the testing feature in Pro Asp.Net MVC book, but I'm having some trouble testing ALL of my routes since some are defined in Areas, not in Global.asax.cs.

I run the AreaRegistration.RegisterAllAreas() method in the Application_Start, not in the RegisterRoutes() method in my application. If I move the call inside RegisterRoutes() or call it from my test method, I can an error stating:

Test method purchase.web.test.RoutingTests.TestQuoteNoProductGroup threw exception: System.Web.HttpException: The type initializer for 'System.Web.Compilation.CompilationLock' threw an exception. ---> System.TypeInitializationException: The type initializer for 'System.Web.Compilation.CompilationLock' threw an exception. ---> System.NullReferenceException: Object reference not set to an instance of an object..

Any thoughts on this error and how I can properly test the routes defined in the other areas?
Requesting Gravatar... Oleg D. Jun 09, 2010 9:53 AM
# re: Testing Routes In ASP.NET MVC
In case somebody needs it, here is how GetProperties() can be implemented:

private static Dictionary<string, object> GetProperties(object values)
{
var result = new Dictionary<string, object>();
if (values != null)
{
foreach (PropertyDescriptor descriptor in TypeDescriptor.GetProperties(values))
{
object obj2 = descriptor.GetValue(values);
result.Add(descriptor.Name, obj2);
}
}
return result;
}

What do you have to say?

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