MS MVC: Testing Routes

In Scott’s post about routing he showed how you can easily test routes. One cool thing about 3.5 asp.net extension framework is the introduction of new interfaces for IHttpContext, IHttpRequest, IHttpResponse, and some other. This makes testing with mocks so much easier. Here I will show you some tests I wrote to validate the routing rules we created for simply restful routing in the ms mvc framework.

What Are We Testing?

When testing we want to stick with one assertion per test. So for this example we are only testing that the rules we wrote set the proper action. You can view the rules we will be testing in my previous post.

Create The Test Fixture

using System.Collections.Specialized;
using System.Web;
using System.Web.Mvc;
using MVCContrib.SimplyRestful;
using NUnit.Framework;
using NUnit.Framework.SyntaxHelpers;
using Rhino.Mocks;

namespace MVCContrib.Specs.SimplyRestfulSpecs
{
  [TestFixture]
  public class SimplyRestfulRouteMatchTests
  {
  }
}

Add The First Test

    [Test]
    public void GetRouteData_WithAControllerAndIdUsingAHttpGetRequest_SetsTheShowAction()
    {
    }

Our test name is using pattern Method_Context_Result. So the method under test is GetRouteData. The context of the test is With a controller and id using a an http get request sets the show action. Now lets fill in the test.

    [Test]
    public void GetRouteData_WithAControllerAndIdUsingAHttpGetRequest_SetsTheShowAction()
    {
      IHttpContext httpContext;
      RouteCollection routeCollection = new RouteCollection();
      SimplyRestfulRouteHandler.InitializeRoutes(routeCollection);
      RouteData routeData = routeCollection.GetRouteData(httpContext);
      Assert.That(routeData.Values["action"], Is.EqualTo("show").IgnoreCase);
    }

Above we wrote our assertion. We want to call the GetRouteData method on a routeCollection and we want to assert that the parameter named “action” is equal to show. At this point the test will fail with a nre on httpContext. httpContext needs to be an instance of IHttpContext, so how do we get an instance of an interface. Simple, we mock it. We will setup the mock context with just enough information for the RouteCollection to do its job. This could be considered a stub and not a mock since we are simply using Rhino.Mocks to setup results.

    [Test]
    public void GetRouteData_WithAControllerAndIdUsingAHttpGetRequest_SetsTheShowAction()
    {
      MockRepository mocks = new MockRepository();
      IHttpContext httpContext = mocks.DynamicMock<IHttpContext>();
      IHttpRequest httpRequest = mocks.DynamicMock<IHttpRequest>();

      RouteCollection routeCollection = new RouteCollection();
      SimplyRestfulRouteHandler.InitializeRoutes(routeCollection);

      using(mocks.Record())
      {
        SetupResult.For(httpContext.Request)
          .Return(httpRequest);
        SetupResult.For(httpRequest.AppRelativeCurrentExecutionFilePath)
          .Return("~/products/123");
        SetupResult.For(httpRequest.HttpMethod)
          .Return("GET");
      }
      using(mocks.Playback())
      {
        RouteData routeData = routeCollection.GetRouteData(httpContext);
        Assert.That(routeData.Values["action"], Is.EqualTo("show").IgnoreCase);
      }
    }

So what just happened? First we created a MockRepository which is the core engine of Rhino.Mocks. This could probably get moved to a [SetUp] method. Next we create two Dynamic Mocks. One for the context and one for the request. We make them dynamic mocks because they are not under test. We do not care what gets called on these interfaces or how it gets called. We just care that if certain properties are called they return a specific values emulating what a real http request would look like. Next we have the mocks.Record phase. This is used to demarcate expectations versus assertions.

What to note is that the RouteCollection uses the AppRelativeCurrentExecutionFilePath property which expects a return string like “~/something/something/something.aspx” we are telling it to return “~/products/123″ products is our controller and 123 should be our id of the route. Next we setup a result for the Httpmethod property, remember our Route had validation on the special name Method which maps to a call on HttpMethod.

Finally we run our test and make the assertions. We enter playback mode, execute the GetRouteData method, which is our method under test, pass in the mocked context and assert that the returned routedata has a parameter named “action” = “show”

So once you add a couple more tests for different routes you will see a way to factor some common code out. I created a helper method to setup my context.

    private void SetupContext(string url, string httpMethod, string formMethod)
    {
      SetupResult.For(httpContext.Request).Return(httpRequest);
      SetupResult.For(httpRequest.AppRelativeCurrentExecutionFilePath).Return(url);
      SetupResult.For(httpRequest.HttpMethod).Return(httpMethod);
      if(!string.IsNullOrEmpty(formMethod))
      {
        NameValueCollection form = new NameValueCollection();
        form.Add("_method", formMethod);
        SetupResult.For(httpRequest.Form).Return(form);
      }
    }

Then my tests simply look like this.

    [Test]
    public void GetRouteData_WithAControllerAndIdUsingFormMethodPut_UsesSimplyRestfulRouteHandler()
    {
      using (mocks.Record())
      {
        SetupContext("~/controller/123", "POST", "PUT");
      }
      using (mocks.Playback())
      {
        RouteData routeData = routeCollection.GetRouteData(httpContext);
        Assert.That(routeData.Route.RouteHandler, Is.EqualTo(typeof(SimplyRestfulRouteHandler)));
      }
    }

The best part about writing tests for this stuff is you no longer have to fire up a browser and test by hand. You get instant verification that runs quick.

If you are wondering how I figured out what to mock and setup results for it was a quick perusal of System.Web.Extensions in reflector. Having to open reflector to figure out what to mock is a code smell for me. You really shouldn’t be mocking things you don’t own or control but this is a nice quick integration test that can save you tons of time down the road. Just don’t be surprised if after an update you get some errors because MS changed the implementation of RouteCollection.GetRouteData, another great reason for using a ContextSetup method.