Showing posts with label routing. Show all posts

Test ASP.NET MVC routes using MVC Contrib

Last time I showed one alternative way of testing routes using Moq and NUnit. It works well but it adds code bloat. The MVC Contrib project has a nice set of test helper methods which simplifies it.

It assumes you use NUnit and RhinoMocks. It also assumes you set your routes in the ASP.NET MVC RouteCollection object. If you reference your routes in your test project like I do, then this shouldn't be a big issue either.

Our previous test method looked like this:


[Test]
public void Test_If_Product_Route_Returns_Expected_Result()
{
RouteCollection routes = new RouteCollection();
POC_Stuff.MvcApplication.RegisterRoutes(routes);

var context = MvcMockHelpers.FakeHttpContext("~/products/videogames/nintendo");
RouteData routeData = routes.GetRouteData(context);

Assert.That(routeData.Values["category"].ToString() == "videogames");
Assert.That(routeData.Values["subcategory"].ToString() == "nintendo");
Assert.That(routeData.Values["controller"].ToString() == "Catalog");
}


The new test method using the MVC Contrib test helpers will look like this:

[Test]
public void Test_If_Product_Route_Returns_Expected_Result()
{
"~/products/videogames/nintendo".ShouldMapTo(x => x.Index("videogames", "nintendo"));
}


I don't think you can make it much more compact! It will even do the assertions for you, the test helper will assert the route data, the controller, the method and its parameters. If you want to make additional asserts, that is possible as well since it returns a RouteData object from the System.Web.Routing namespace.

The main test class in the MVC Contrib project is called RouteTestingExtensionsand the method that will assert the route for us is called ShouldMapTo and the code as of when this post was written looks like this:


///
/// Asserts that the route matches the expression specified. Checks controller, action, and any method arguments
/// into the action as route values.
///

/// The controller.
/// The routeData to check
/// The action to call on TController.
public static RouteData ShouldMapTo(this RouteData routeData, Expression> action)
where TController : Controller
{
routeData.ShouldNotBeNull("The URL did not match any route");

//check controller
routeData.ShouldMapTo();

//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 = null;

switch ( methodCall.Arguments[ i ].NodeType )
{
case ExpressionType.Constant:
value = ( (ConstantExpression)methodCall.Arguments[ i ] ).Value;
break;
case ExpressionType.MemberAccess:
value = Expression.Lambda(methodCall.Arguments[ i ]).Compile().DynamicInvoke();
break;
}
value = (value == null ? value : value.ToString());
routeData.Values.GetValue(name).ShouldEqual(value,"Value for parameter did not match");


These doesn't seem to be too many developers in the community talking about this great test helper but I hope you see the value it adds.

Routes in ASP.NET MVC

The routing enginge that ASP.NET MVC is quite powerful and allows mapping between an URL and the controller and controller action. The routing was originally released in ASP.NET 3.5 SP 1 but I suspect few use it with WebForms. You can easily add new routes and have multiple different routes.

As an example, let's assume we want to have a route where we easily can list various products and also a separate route for events that we offer during various dates. A good piece of advice by Scott Hanselman is to add a comment above your route to make it more clear what the expected route is which I normally do. All the routes are normally wired in the global.asax.

Our first route to display a subcategory will look like this: http://localhost/product/videogames/nintendo . The crafted route will look like this.


// http://localhost/product/category/subcategory
// http://localhost/product/videogames/nintendo
routes.MapRoute(
"Category", // Route name
"products/{category}/{subcategory}", // URL with parameters
new
{
controller = "Catalog", action = "Index", category = "Listed",
subcategory = "All Items"
}
);


We added defaults for the controller, action, category and subcategory but of course that is optional. Our controllers will accept the category and subcategory and act on it.

Our controller class will need to get the category and subcategory to be able to act on it. Our controller in this example looks like this:


public class CatalogController : Controller
{
//
// GET: /Catalog/

public ActionResult Index(string category, string subcategory)
{
ViewData["Message"] = string.Format("Category {0}, Subcategory{1}"
, category, subcategory);
return View();
}

}


In our next route, we want it to be displaying a specific event on a specific date and as an example it will look like this: http://localhost/events/2009/05/25 . We will craft it like this:


// http://localhost/events/year/month/day
// http://localhost/events/2009/05/25
routes.MapRoute(
"Event", // Route name
"events/{year}/{month}/{day}", // URL with parameters
new
{
controller = "Event",
action = "Index"
}
);


A corresponding controller that will be handling this request has to be able to handle the year, month and day:


public class EventController : Controller
{
//
// GET: /Event/

public ActionResult Index(string year, string month, string day)
{
ViewData["Message"] = string.Format("Event for mm {0} / dd {1} / yyyy {2}"
, month, day, year);
return View();
}

}


If you have many routes set up, it may be difficult to figure out why a route isn't being executed as expected. I hope most people by now are familiar with Phil Haack's excellent URL Routing Debugger.

If we try to hit our product URL by incorrectly specifying the URL http://localhost/product, the Routing Debugger shows that the expected route is not being routed correctly.


This can save a ton of time if you have many routes set up in the same project.

The one thing we're still missing is unit tests. We should have started with the unit tests but in this example we're showing the example in reverse order to make the routing mechanism more clear. The ASP.NET MVC framework supports many different unit test and mocking frameworks, I prefer the combination of NUnit and Moq but you can use any of the other supported .Net unit testing/mocking frameworks.

In order to test the routing mechanism, we need to be able to mock the HttpContext. I prefer to use the class MvcMockHelpersthat Scott Hanselman has blogged about instead of writing it from scratch. Our test case for the product route will be quite simple since the plumbing is already done:


using System;
using System.Web;
using System.Web.Routing;
using Moq;
using NUnit.Framework;

[TestFixture]
public class RouteTest
{
[Test]
public void Test_If_Product_Route_Returns_Expected_Result()
{
RouteCollection routes = new RouteCollection();
POC_Stuff.MvcApplication.RegisterRoutes(routes);

var context = MvcMockHelpers.FakeHttpContext("~/products/videogames/nintendo");
RouteData routeData = routes.GetRouteData(context);

Assert.That(routeData.Values["category"].ToString() == "videogames");
Assert.That(routeData.Values["subcategory"].ToString() == "nintendo");
Assert.That(routeData.Values["controller"].ToString() == "Catalog");
}
}


We call the RegisterRoute method of the Global.asax to get the actual routes. After we get the fake HttpContext with our URL, we just need to verify if we see the expected result.

There are other ways to set up the routing with some of the other frameworks like MvcContrib which we will look at next time.