In ASP.NET MVC, getting your routes setup properly can be tricky if you have a lot of routes. Whenever something is tricky, and might be touched often, having a set of unit tests around it can very helpful to ensure you aren’t breaking it when you make changes. Lots of routes can definitely qualify; your users rely on unbroken links and stable URLs for a good experience.
The techniques shown here not only work for unit testing, but can also be used outside of the ASP.NET pipeline when you need to interact with the routing system (for example, to generate URLs from a service). The routing system used by ASP.NET MVC uses the abstraction classes, so the actual running ASP.NET pipline isn’t required.
Stubbing
The key to getting all this to work is the use of stub versions of the HTTP abstraction classes (namely, context, request, and response objects). The routing system uses very little of these actual classes, so the stubs are fairly simple. The easiest way to figure out what’s being used is to use a mock framework in strict mode and see which requests fail.
The end of this blog post contains the source to the stubs we’ll be using below. These stubs work for both incoming and outgoing routing tests.
Incoming Routes
Testing incoming routes means feeding a request URL into the routing system and looking at the route values that come back out the other side.
Our stubs will enable us to write tests like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
[TestMethod] public void RouteWithControllerNoActionNoId() { // Arrange var context = new StubHttpContextForRouting(requestUrl: "~/controller1"); var routes = new RouteCollection(); MvcApplication.RegisterRoutes(routes); // Act RouteData routeData = routes.GetRouteData(context); // Assert Assert.IsNotNull(routeData); Assert.AreEqual("controller1", routeData.Values["controller"]); Assert.AreEqual("Index", routeData.Values["action"]); Assert.AreEqual(UrlParameter.Optional, routeData.Values["id"]); } [TestMethod] public void RouteWithControllerWithActionNoId() { // Arrange var context = new StubHttpContextForRouting(requestUrl: "~/controller1/action2"); var routes = new RouteCollection(); MvcApplication.RegisterRoutes(routes); // Act RouteData routeData = routes.GetRouteData(context); // Assert Assert.IsNotNull(routeData); Assert.AreEqual("controller1", routeData.Values["controller"]); Assert.AreEqual("action2", routeData.Values["action"]); Assert.AreEqual(UrlParameter.Optional, routeData.Values["id"]); } [TestMethod] public void RouteWithControllerWithActionWithId() { // Arrange var context = new StubHttpContextForRouting(requestUrl: "~/controller1/action2/id3"); var routes = new RouteCollection(); MvcApplication.RegisterRoutes(routes); // Act RouteData routeData = routes.GetRouteData(context); // Assert Assert.IsNotNull(routeData); Assert.AreEqual("controller1", routeData.Values["controller"]); Assert.AreEqual("action2", routeData.Values["action"]); Assert.AreEqual("id3", routeData.Values["id"]); } [TestMethod] public void RouteWithTooManySegments() { // Arrange var context = new StubHttpContextForRouting(requestUrl: "~/a/b/c/d"); var routes = new RouteCollection(); MvcApplication.RegisterRoutes(routes); // Act RouteData routeData = routes.GetRouteData(context); // Assert Assert.IsNull(routeData); } [TestMethod] public void RouteForEmbeddedResource() { // Arrange var context = new StubHttpContextForRouting(requestUrl: "~/foo.axd/bar/baz/biff"); var routes = new RouteCollection(); MvcApplication.RegisterRoutes(routes); // Act RouteData routeData = routes.GetRouteData(context); // Assert Assert.IsNotNull(routeData); Assert.IsInstanceOfType(routeData.RouteHandler, typeof(StopRoutingHandler)); } |
Outgoing Routes
Testing outgoing routes means using the UrlHelper class to create URLs, given a specific set of values. With the stubs provided, you could also use the UrlHelper class in a non-MVC project to generate URLs for you. All you need access to is the routes and the app path where the application is running.
Our stubs will enable us to write tests like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
[TestMethod] public void ActionWithAmbientControllerSpecificAction() { UrlHelper helper = GetUrlHelper(); string url = helper.Action("action"); Assert.AreEqual("/defaultcontroller/action", url); } [TestMethod] public void ActionWithSpecificControllerAndAction() { UrlHelper helper = GetUrlHelper(); string url = helper.Action("action", "controller"); Assert.AreEqual("/controller/action", url); } [TestMethod] public void ActionWithSpecificControllerActionAndId() { UrlHelper helper = GetUrlHelper(); string url = helper.Action("action", "controller", new { id = 42 }); Assert.AreEqual("/controller/action/42", url); } [TestMethod] public void RouteUrlWithAmbientValues() { UrlHelper helper = GetUrlHelper(); string url = helper.RouteUrl(new { }); Assert.AreEqual("/defaultcontroller/defaultaction", url); } [TestMethod] public void RouteUrlWithAmbientValuesInSubApplication() { UrlHelper helper = GetUrlHelper(appPath: "/subapp"); string url = helper.RouteUrl(new { }); Assert.AreEqual("/subapp/defaultcontroller/defaultaction", url); } [TestMethod] public void RouteUrlWithNewValuesOverridesAmbientValues() { UrlHelper helper = GetUrlHelper(); string url = helper.RouteUrl(new { controller = "controller", action = "action" }); Assert.AreEqual("/controller/action", url); } |
The implementation of GetUrlHelper looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
static UrlHelper GetUrlHelper(string appPath = "/", RouteCollection routes = null) { if (routes == null) { routes = new RouteCollection(); MvcApplication.RegisterRoutes(routes); } HttpContextBase httpContext = new StubHttpContextForRouting(appPath); RouteData routeData = new RouteData(); routeData.Values.Add("controller", "defaultcontroller"); routeData.Values.Add("action", "defaultaction"); RequestContext requestContext = new RequestContext(httpContext, routeData); UrlHelper helper = new UrlHelper(requestContext, routes); return helper; } |
Stub Source
This one set of stubs can be used for both incoming and outgoing route testing:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
using System.Collections.Specialized; using System.Web; public class StubHttpContextForRouting : HttpContextBase { StubHttpRequestForRouting _request; StubHttpResponseForRouting _response; public StubHttpContextForRouting(string appPath = "/", string requestUrl = "~/") { _request = new StubHttpRequestForRouting(appPath, requestUrl); _response = new StubHttpResponseForRouting(); } public override HttpRequestBase Request { get { return _request; } } public override HttpResponseBase Response { get { return _response; } } } public class StubHttpRequestForRouting : HttpRequestBase { string _appPath; string _requestUrl; public StubHttpRequestForRouting(string appPath, string requestUrl) { _appPath = appPath; _requestUrl = requestUrl; } public override string ApplicationPath { get { return _appPath; } } public override string AppRelativeCurrentExecutionFilePath { get { return _requestUrl; } } public override string PathInfo { get { return ""; } } public override NameValueCollection ServerVariables { get { return new NameValueCollection(); } } } public class StubHttpResponseForRouting : HttpResponseBase { public override string ApplyAppPathModifier(string virtualPath) { return virtualPath; } } |