Archive for the ‘ASP.Net MVC’ Category

Manually unit testing routes in ASP.Net MVC

November 10, 2009

Wow, been a while since I blogged anything.  I’ve honestly been intending on opening up a new blog site that isn’t hosted on wordpress (using my own domain, etc) but just haven’t gotten around to it. So I’ll post this here and then move it when I finally get around to setting up the new blog.

The issue of this post is really a gut-level demo of how to unit test routes. I know that MVC Contrib offers some good helpers for doing it, but I was recently preparing an ASP.Net MVC class and I wanted a way of showing the students exactly what goes into the routing engine’s logic, and I felt that manually setting up testing was a good way to walk them through it.

In order to maximize reuse, and reduce code and so on, I chose to create an abstract base class that handled all the wiring of routes and encapsulated a few common operations critical to testing.   The base class is intended as the base of your test class, and so whatever classes you’re testing routes with can inherit from it.  Much of the concepts used in creating this class are thanks to Steve Sanderson’s wonderful Pro ASP.Net MVC book by apress.

Why that and not extension methods?  Well, I simply wanted to have more control over the flow of logic to let each test be more focused. 

Without further ado…

Routing, as you may know, involves both inbound and outbound steps.  Inbound routing happens when the system wants to take a URL that someone’s trying to surf to, and turn it into an object representing the routedata associated with that URL (e.g. info about the controller, action, any input params, etc).

Outbound routing is the opposite, generating URLs based on a given route data object.  Bidirectional testing is critical.  This abstract class supports both, using the Moq framework. 

I’ll cover usage of my class for now, and save an understanding of what my class is doing for another post (though I think it should be pretty apparent; there’s not much magic in there). 

To use this class, you just have to inherit from it, and implement one abstract method. 


[TestClass]
public class MyRouteTests : RouteTestBase
{
protected override void RegisterRoutes(System.Web.Routing.RouteCollection cfg)
{
MvcApplication.RegisterRoutes(cfg);
}
}

that override lets you customize the means by which your app registers its routes.  In htis case, my demo app was using the static method provided by the default MVC template, but obviously I could use other means if needed.

Once I’ve done that, I would want to setup a test for an inbound route like so:


[TestMethod]
public void SlashGoesToHomeIndex()
{
//Arrange
string path = "~/";

// Act
RouteData data = GetRouteData(path);

// Assert
CoreInboundUrlAsserts(data, "home", "index");
}

Which (if you look at the base class) asserts that my route configuration returned a route data object, an dthat route data object mapped to the HomeController’s Index action.

Doing a simple outbout route test is similarly straightforward:


[TestMethod]
public void HomeIndexGoesToSlash()
{
//Arrange
string expected = "/";
var data = new { controller = "home", action = "index", id = "" };

// Act
var virtualPath = GenerateUrlViaMock(data);

// Assert
Assert.AreEqual(expected, virtualPath.VirtualPath);
}

And finally, the whole base class looks like this:


public abstract class RouteTestBase
{
protected const string GET = "GET";
protected const string POST = "POST";

protected void CoreInboundUrlAsserts(RouteData data, string controller, string action)
{
Assert.IsNotNull(data, “NULL Route data was returned”);
Assert.IsNotNull(data.Route, “No route was matched”);
Assert.AreEqual(controller, data.Values[“controller”].ToString(), true, “Wrong controller”);
Assert.AreEqual(action, data.Values[“action”].ToString(), true, “Wrong action”);
}

protected RouteData GetRouteData(string path)
{
return GetRouteData(path, GET);
}

protected RouteData GetRouteData(string path, string httpMethod)
{
RouteCollection cfg = new RouteCollection();
RegisterRoutes(cfg);
var ctx = MakeMockHttpContext(path,httpMethod);
return cfg.GetRouteData(ctx.Object);
}

protected abstract void RegisterRoutes(RouteCollection cfg);

protected Mock MakeMockHttpContext(string url)
{
return MakeMockHttpContext(url, GET);
}

protected Mock MakeMockHttpContext(string url, string httpMethod)
{
var mockHttpContext = new Mock();
MockRequest(url, mockHttpContext, httpMethod);

// Mock response
MockResponse(mockHttpContext);

return mockHttpContext;
}

protected virtual void MockResponse(Mock mockHttpContext)
{
var mockResponse = new Mock();
mockHttpContext.Setup(c => c.Response).Returns(mockResponse.Object);
mockResponse.Setup(r => r.ApplyAppPathModifier(It.IsAny())).Returns(r => r);
MockResponseTestSetups(mockResponse);
}

protected virtual void MockRequest(string url, Mock mockHttpContext, string method)
{
Mock mockRequest = new Mock();
mockHttpContext.Setup(c => c.Request).Returns(mockRequest.Object);
mockRequest.Setup(r => r.AppRelativeCurrentExecutionFilePath).Returns(url);
mockRequest.Setup(r => r.RequestType).Returns(method);
mockRequest.Setup(r => r.HttpMethod).Returns(method);
MockRequestTestSetups(mockRequest);
}

///
/// When overridden in child classses, adds additional setups to customize the request mock
///
///
protected virtual void MockRequestTestSetups(Mock mockRequest)
{
// default do nothing unless the subclass wants to
}

///
/// When overridden in child classses, adds additional setups to customize the response mock
///
///
protected virtual void MockResponseTestSetups(Mock mockResponse)
{
// default do nothing unless the subclass wants to
}

protected VirtualPathData GenerateUrlViaMock(object values)
{
// Arrange
RouteCollection cfg = new RouteCollection();
RegisterRoutes(cfg);
var ctx = MakeMockHttpContext(null);
RequestContext context = new RequestContext(ctx.Object, new RouteData());

// Act
return cfg.GetVirtualPath(context, new RouteValueDictionary(values));
}
}