Fluent Route Testing in ASP.NET MVC
Tuesday, November 25 2008 27 Comments
Have you written code like this recently?
[Test]
public void tedious_route_test()
{
Global.RegisterRoutes(RouteTable.Routes);
var httpContext = MockRepository.GenerateStub<HttpContextBase>();
httpContext.Stub(x => x.Request).Return(MockRepository.GenerateStub<HttpRequestBase>());
httpContext.Request.Stub(x => x.PathInfo).Return("");
httpContext.Request.Stub(x => x.AppRelativeCurrentExecutionFilePath).Return("~/foo/bar");
var routeData = RouteTable.Routes.GetRouteData(httpContext);
Assert.That(routeData.Values["controller"], Is.EqualTo("foo"));
Assert.That(routeData.Values["action"], Is.EqualTo("bar"));
}
This mess of nastiness is what is required to test your routes in ASP.NET MVC. There are a number of things wrong with this. First and foremost, we're doing magical-mocking here. That is - we somehow know exactly the two properties we need to mock for this to work. It turns out this isn't such a magical process as it is looking at reflector and a lot of trial and error. The next thing wrong that I see is that our route values (and keys) are case-insensitive, so we'd need to include some more flexibility in our Asserts here.
Who wants to deal with that mess? I sure don't. Last night, my pal & co-author Jimmy Bogard banged together a fluent API for testing routes without needing all of this crap.
Here is a test to verify our root route is working properly:
"~/".Route().ShouldMapTo<FooController>();
And a more complex case where we have an action parameter:
"~/foo/bar/widget".Route().ShouldMapTo<FooController>(x => x.Bar("widget"));
These are a lot nicer, as we are dealing with no unnecessary string here, the controller are now strongly-typed (we get intellisense and refactoring support), and we can easily test routes with a single line.
This does have a couple caveats though: it assumes
- you're using NUnit
- you're using RhinoMocks
- you add your routes to the RouteTable.Routes collection
So how did we do this? I have to admit, I tried to get this to work on the bus ride to work, but without an internet connection I was dead in the water. Jimmy helped me grasp the way that Expression<T> works. His wise words were: "Here's where you just set a breakpoint and fire up the debugger and look." He's right - the API there is quite opaque. But once I got it to work it made a lot of sense. Expression<T> is awesome!
Here's the method with most of the goods:
public static RouteData ShouldMapTo<TController>(this RouteData routeData, Expression<Func<TController, ActionResult>> action)
where TController : Controller
{
Assert.That(routeData, Is.Not.Null, "The URL did not match any route");
//check controller
routeData.ShouldMapTo<TController>();
//check action
var methodCall = (MethodCallExpression) action.Body;
string actualAction = routeData.Values.GetValue("action").ToString();
string expectedAction = methodCall.Method.Name;
actualAction.AssertSameStringAs(expectedAction);
//check parameters
for (int i = 0; i < methodCall.Arguments.Count; i++)
{
string name = methodCall.Method.GetParameters()[i].Name;
object value = ((ConstantExpression) methodCall.Arguments[i]).Value;
Assert.That(routeData.Values.GetValue(name), Is.EqualTo(value.ToString()));
}
return routeData;
}
This might be a tad brittle, but in my preliminary testing it worked wonders. This code is now in MvcContrib's TestHelper project.


Ze Pedro
11.26.2008
4:06 AM
Awesome! Thank you so much for finally allowing me to grasp Expression<T>.
You made my day! Many thanks from Portugal.
Keep up the good work!
Ze