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.
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");
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.