4. August 2013

Mocking Controller Context with HTTP Request for Unit Testing ApiController of ASP.NET WebApi

For a new project I am using ASP.NET Web API. Web API is a great way to implement REST services.

Web API makes many things easier, but there is one thing missing: simple unit testing of controllers. You can definitely call controller methods like Get() and Get(id). But it is not easy to prepare the controller's server context so, that Request.CreateResponse() works correctly. Also, if you want to return the URI of a newly created (Post-ed) item, then a lot of wiring has to be done around ApiController's properties Request and ControllerContext,

Here is my solution: I am extending ApiController with a MockRequest function, which sets up the routing stuff like routes, route data, request URI. The goal is, that the controller method can:
- check the request URI including the query,
- create URLs using the route template,
- create a HttpResponseMessage as result.

The MockRequest method is called like:
var controller = new ProductsController(productRepository);
controller.MockRequest(HttpMethod.Get, new { controller = "products", id = 1 });
Product result = controller.Get(1);
Assert.Equal(result.Name, "Tomato Soup");
Implementation:
public static class ApiControllerExtensions
{
  public static void MockRequest(this ApiController self, HttpMethod httpMethod, object routeValues)
  {
    var routeValueDict = new HttpRouteValueDictionary();
    foreach (var prop in routeValues.GetType().GetProperties()) {
      routeValueDict.Add(prop.Name, prop.GetValue(routeValues, null));
    }
    var config = new HttpConfiguration();
    var request = new HttpRequestMessage { Method = httpMethod, RequestUri = new Uri("http://localhost/") };
    var route = config.Routes.MapHttpRoute("DefaultApi", "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional });
    var routeData = new HttpRouteData(route, routeValueDict);
    self.ControllerContext = new HttpControllerContext(config, routeData, request);
    self.Request = request;
    self.Request.Properties.Add(HttpPropertyKeys.HttpRouteDataKey, routeData);
    self.Request.Properties[HttpPropertyKeys.HttpConfigurationKey] = config;
    self.Request.RequestUri = new Uri(self.Url.Link("DefaultApi", routeValues));
  }
}

Note: the argument routeValues also takes additional properties, which are automatically converted to query parameters, because they do not fit the route template. Just as it should be.

Note: the route template is fixed to "api/{controller}/{id}". If you have a different template, change it or make it a parameter of MockRequest. I did not want too many variables, because my project only has /api/controller[/id] URIs.

Note: don't ask me why the request is set up with a default URI "http://localhost/", then later the RequestURI property is set explicitly. Without the prior "http://localhost/", self.Url.Link() always returns null.

Note: there is more to do if you want to unit test model validation as well. See Unit test WebAPI Controllers with Model Validation. Looks good, but does not compile with my NET 4.0. If you also want to check the formatting, then you might need to talk to an in memory HTTP server (which will also include the model validation).

_happy_routing()

Keine Kommentare: