Testing routes in ASP.NET Web API

Β· 1356 words Β· 7 minutes to read

The question that often comes up when talking to developers and clients about Web API solutions is how exactly should you go about testing your route configuration in Web API? Some would perhaps argue that in certain cases, especially if you stick to RESTful approach, this type of testing wouldn’t even be necessary, because the convention over configuration provided by the framework means that you effecitvely end up testing something that’s internal working of Web API.

With that said, especially when you have complex routes, or when you break the Restful approach and provide RPC-style API, or if you have your API actions decorated with HTTP verbs that don’t match the action names, you probably want to (and probably should, if you ask me) test the API routing to make sure certain requests end up in proper places.

Let’s deal with this interesting problem.

Setting up the tests πŸ”—

Testing routing in Web API is relatively easy, but unfortunately, contrary to many other things Web API offers it’s not available out of the box and requires a little bit of helper code. Effecitvely what we need to do, is to utilize a bit of the framework’s APIs that’s used in its processing pipeline.

We will have to mock a few things, from the HTTP request that’s expected, through route data, to things like controller selector, action selector and controller context.

Because we will be using a few pieces that are interdependent on each other (probably because they were designed to be used i nthe processing pippeline rather than for test mocks), the easiest way is to create a helper class to wrap all that and expose just the meaningful public methods through that class.

public class RouteTester  
{  
HttpConfiguration config;  
HttpRequestMessage request;  
IHttpRouteData routeData;  
IHttpControllerSelector controllerSelector;  
HttpControllerContext controllerContext;

public RouteTester(HttpConfiguration conf, HttpRequestMessage req)  
{  
config = conf;  
request = req;  
}

public string GetActionName()  
{  
//DO STUFF  
}

public Type GetControllerType()  
{  
//DO STUFF  
}  
}  

This is our route testing class. What we want to be able to do is:

  • pass it an instance of HttpConfiguration - this is where our routing is defined
  • pass it an instance of HttpRequestMessage - we need it to encapsulate the url and HTTP method

Our testing class will expose 2 public method. First will return the Type of the controller determined for the given URL+Http method combination. The other will return the action name as string, again determined for the given URL+Http method combination.

I also already included a number of local fields which we will use later.

Building the Route Tester πŸ”—

Let’s go back to the constructor. It’s rather self explanatory what happens so far, as we just assign internally the paramteres passed by the caller. However, we must initialize some additional internal properties as well.

public RouteTester(HttpConfiguration conf, HttpRequestMessage req)  
{  
config = conf;  
request = req;  
routeData = config.Routes.GetRouteData(request);  
request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData;  
controllerSelector = new DefaultHttpControllerSelector(config);  
controllerContext = new HttpControllerContext(config, routeData, request);  
}  

To explain why we do that, let’s look at the problem backwards. Ultimately, we want our class to be able to give us the Type of the controller 1 and name of the action 2. To do that we need:

  1. an instance of HttpControllerDescriptor
  2. an instance of HttpActionDescriptor

To get 1, we need an instance of DefaultHttpControllerSelector which is why we init it in the constructor. To get 2 we’ll need an HttpControllerContext, so that’s also getting initialized in the constructor.

Now, for both of these to be used in proper context, we have to get the route data for the given request beforehand and set it as a property of the request, and that’s exactly what’s being done in the constructor as well.

Once that’s in place we can implement the public method. First the controller getter:

public Type GetControllerType()  
{  
var descriptor = controllerSelector.SelectController(request);  
controllerContext.ControllerDescriptor = descriptor;  
return descriptor.ControllerType;  
}  

We use controller selector (if you recall, initialized in the class’ constructor) to select the proper controller for a given request (very important - that’s why we set the route data as property of the request, otherwise this would return null as there would be no routing context). We could then return the controller type straight away, but we set the ControllerDescriptor of the controllerContext field - this will be used later to determine the action that’s used, so let’s store it for that purpose. That’s one example of the spaghettish API we have to use to provide this route testing functionality, and why it’s better to lock this implementation in its own class.

The action getter is as follows:

public string GetActionName()  
{  
if (controllerContext.ControllerDescriptor == null)  
GetControllerType();

var actionSelector = new ApiControllerActionSelector();  
var descriptor = actionSelector.SelectAction(controllerContext);

return descriptor.ActionName;  
}  

Since it relies on the ControllerDescriptor to be set within the local controllerContext, we may need to go through the other method if that’s null in the beginning.

After that’s it’s straight forward, just return the action name through the action selector that’s relevant for the given controller context.

Usage πŸ”—

Now we can start using our testing class. I will use my favorite testing framework, xUnit, but obviously you can go with whichever suits you best.

public class RouteTests  
{  
HttpConfiguration _config;

public RouteTests()  
{  
_config = new HttpConfiguration();  
_config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;  
_config.Routes.MapHttpRoute(name: "Default", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional });  
_config.Routes.MapHttpRoute(name: "DefaultRPC", routeTemplate: "api/v2/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional });  
_config.Routes.MapHttpRoute(name: "DefaultRPC", routeTemplate: "api/v2/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional });  
}

//TESTS METHODS GO HERE  
}  

In the test’s constructor (setup), we configure our HttpConfiguration, since it will be reusable between test methods.

Example test:

[Fact]  
public void UrlControllerGetIsCorrect()  
{  
var request = new HttpRequestMessage(HttpMethod.Get, "/api/url/");

var routeTester = new RouteTester(_config, request);

Assert.Equal(typeof(UrlController), routeTester.GetControllerType());  
Assert.Equal(ReflectionHelpers.GetMethodName((UrlController p) => p.Get()), routeTester.GetActionName());  
}  

So we construct a simple HttpRequestMessage, passing to it an Http method to be used and the URL. Note, it needs to have athe host part of the Uri to be set to something - doesn’t matter what - but relative urls will throw 404 error.

Then we create an instance of our RouteTester and compare its public methods’ results with our expected outcome. I am using a reflection helper method to be able to pass method name in a strongly typed lambda manner, but you could hardcode it as a string as well. I just think such strongly typed approach is cleaner and more functional.

So, the helper code, for the record:

public class ReflectionHelpers  
{  
public static string GetMethodName<T, U>(Expression<Func<T, U>> expression)  
{  
var method = expression.Body as MethodCallExpression;  
if (method != null)  
return method.Method.Name;

throw new ArgumentException("Expression is wrong");  
}  
}  

One more example test, this time for a POST method and a different API route:

[Fact]  
public void V2\_RPC\_UrlControllerPostIsCorrect()  
{  
var request = new HttpRequestMessage(HttpMethod.Post, "/api/v2/url/Add");

var routeTester = new RouteTester(_config, request);

Assert.Equal(typeof(UrlController), routeTester.GetControllerType());  
Assert.Equal(ReflectionHelpers.GetMethodName((UrlController p) => p.Add(new Url())), routeTester.GetActionName());  
}  

Note that it’s just the coincidence that my controller’s in this test (and the models) are called UrlController and Url. That has nothing to do with routing, it’s just I pulled them from an application that’s a url-shortening service.

Now if you run the tests you can see them pass nicely.

Summary and source code πŸ”—

You can see that with a small class which hides this quite verbose and repetitive code required to set up route testing, it can actually be a pretty slick and easy to perform task. From my experience I can assure you that route testing is extremely valuable, especially combined with the strongly typed action comparison like the one I showed in the examples. If you have a lot of controllers and a lot of routes, you could very easily see how much your latest change affected the other parts of your application, and ultiamtely, that’s what testing is all about.

There is not much source code to go with this post to be honest, you could proabbly easily copy and paste stuff from the snippets, but if you want to have the source I added it to my “in memory” testing project on github, so please be welcome to grab that.

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP