Fickle Bits

You're doing it wrong.

Fluent Route Testing in ASP.NET MVC

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. 

Technorati Tags:

Comments