Unit testing URL generation and Html.ActionLink in ASP.NET MVC
Posted at 08:00 on 27 October 2008
As I've been working with ASP.NET MVC lately on a couple of websites, the one thing I've found the hardest to get used to is the routing engine. Getting it set up to parse your URL to give you a route is straightforward enough -- the hard, and often confusing, part is the helper functions such as Html.ActionLink
that generate URLs from route data. Sometimes the URLs look different to what you expect, but they work nonetheless; at other times, they are just plain wrong, especially if you have a complex routing table set up.
The logic behind constructing the URLs is fairly complex, and depends not only on the route data that you pass into the ActionLink
method, but also on the route data that comes from the original URL that you used to access the page in the first place. If you have routes that are any more complex than the fairly trivial examples that come in the out of the box application templates, it can quickly get pretty confusing. Furthermore, chopping and changing the order in which you register your routes can get things totally out of kilter, so you really need a comprehensive suite of unit tests to be able to tackle it with any hope of retaining your sanity whatsoever.
The subject of unit testing your routes to make sure that you are getting the correct route data out of them has been covered by Phil Haack and Stephen Walther, so I won't go into any further detail about that aspect here. However, I'm going to expand a bit on Phil's methods to show how to test things the other way round: making sure that when you pass some route data in to Html.ActionLink
, it gives you the URL that you expect.
As with Phil's sample code, I've used Moq to mock the context, request and response, and I'm using Eilon Lipton's technique of using an anonymous class as a dictionary literal. You can download the code as a Visual Studio solution if you want to get up and running with it straight away. Here's a quick look at the methods that do all the work:
string FindUrlToRoute(string currentPage, object routeData) { var mockContext = new Mock<httpContextBase>(); var mockRequest = new Mock<httpRequestBase>(); var mockResponse = new Mock<httpResponseBase>(); mockContext.Expect(c => c.Request).Returns(mockRequest.Object); mockContext.Expect(c => c.Response).Returns(mockResponse.Object); mockRequest.Expect(c => c.AppRelativeCurrentExecutionFilePath) .Returns(currentPage); mockResponse .Expect(c => c.ApplyAppPathModifier(It.IsAny<string>())) .Returns((string s) => s); var route = routes.GetRouteData(mockContext.Object); var requestContext = new RequestContext(mockContext.Object, route); var url = new UrlHelper(requestContext); var dict = new RouteValueDictionary(); foreach (PropertyValue property in GetProperties(routeData)) { dict[property.Name] = property.Value; } var path = routes.GetVirtualPath(requestContext, dict); return (path != null ? path.VirtualPath : null); } protected void AssertRouteUrl(string currentPage, string expectedUrl, object routeData) { var str = FindUrlToRoute(currentPage, routeData); Assert.AreEqual(expectedUrl, str, "URL was wrong!"); }
The sharp eyed among you will note that I am not actually calling Html.ActionLink()
itself, but a different method, routes.GetVirtualPath
. When you construct an action link, or when you use Url.Action(...)
in your views, ASP.NET MVC ends up one way or another running your data through the GetVirtualPath
method of your route table. You have to mock the Response.ApplyAppPathModifier
method -- a fact that wasn't immediately obvious, and it took a bit of digging around in the System.Web.Routing
assembly with Reflector to find out exactly what needed to be done.
You can then check to see whether you are getting the correct URL out by calling in to the AssertRouteUrl
method as follows:
[Test] public void TestBlogPath() { AssertRouteUrl( /* * currentPage is an app-relative URL, so it must be * prefixed with a tilde (~). Note that this is required * and must point to a valid route. */ "~/blog/2008/10/25", /* * expectedUrl, on the other hand, is an absolute path, * so it shouldn't. */ "/blog/2008/10/10", /* * The route data. Note that you specify your controller and * action here as the controller and action properties * respectively. */ new { controller = "blog", action = "index", year = "2008", month = "10", day = "10" } ); }
Download: Unit testing route URL generation - Visual Studio solution