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.