在ASP.NET WebApi中testing路由configuration

我正在尝试对我的WebApi路由configuration进行一些unit testing。 我想testing路由"/api/super"映射到我的SuperControllerGet()方法。 我已经安装了下面的testing,并有几个问题。

 public void GetTest() { var url = "~/api/super"; var routeCollection = new HttpRouteCollection(); routeCollection.MapHttpRoute("DefaultApi", "api/{controller}/"); var httpConfig = new HttpConfiguration(routeCollection); var request = new HttpRequestMessage(HttpMethod.Get, url); // exception when url = "/api/super" // can get around w/ setting url = "http://localhost/api/super" var routeData = httpConfig.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; var controllerSelector = new DefaultHttpControllerSelector(httpConfig); var controlleDescriptor = controllerSelector.SelectController(request); var controllerContext = new HttpControllerContext(httpConfig, routeData, request); controllerContext.ControllerDescriptor = controlleDescriptor; var selector = new ApiControllerActionSelector(); var actionDescriptor = selector.SelectAction(controllerContext); Assert.AreEqual(typeof(SuperController), controlleDescriptor.ControllerType); Assert.IsTrue(actionDescriptor.ActionName == "Get"); } 

我的第一个问题是,如果我没有指定一个完全合格的URL httpConfig.Routes.GetRouteData(request); 抛出一个InvalidOperationExceptionexception,并显示消息“此操作不支持相对URI”。

我明显失去了我的残肢configuration。 我宁愿使用相对URI,因为使用完全限定的URI进行路由testing似乎并不合理。

我的第二个问题与我的configuration上面是我没有testing我的路线configuration在我的RouteConfig,但我使用:

 var routeCollection = new HttpRouteCollection(); routeCollection.MapHttpRoute("DefaultApi", "api/{controller}/"); 

如何使用在典型的Global.asax中configuration的分配的RouteTable.Routes

 public class MvcApplication : HttpApplication { protected void Application_Start() { // other startup stuff RouteConfig.RegisterRoutes(RouteTable.Routes); } } public class RouteConfig { public static void RegisterRoutes(RouteCollection routes) { // route configuration } } 

更进一步,我上面所提到的可能不是最好的testingconfiguration。 如果有一个更简化的方法,我是全部耳朵。

我最近正在testing我的Web API路由,这是我如何做到的。

  1. 首先,我创build了一个帮助器来移动所有的Web API路由逻辑:
  public static class WebApi { public static RouteInfo RouteRequest(HttpConfiguration config, HttpRequestMessage request) { // create context var controllerContext = new HttpControllerContext(config, Substitute.For<IHttpRouteData>(), request); // get route data var routeData = config.Routes.GetRouteData(request); RemoveOptionalRoutingParameters(routeData.Values); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; controllerContext.RouteData = routeData; // get controller type var controllerDescriptor = new DefaultHttpControllerSelector(config).SelectController(request); controllerContext.ControllerDescriptor = controllerDescriptor; // get action name var actionMapping = new ApiControllerActionSelector().SelectAction(controllerContext); return new RouteInfo { Controller = controllerDescriptor.ControllerType, Action = actionMapping.ActionName }; } private static void RemoveOptionalRoutingParameters(IDictionary<string, object> routeValues) { var optionalParams = routeValues .Where(x => x.Value == RouteParameter.Optional) .Select(x => x.Key) .ToList(); foreach (var key in optionalParams) { routeValues.Remove(key); } } } public class RouteInfo { public Type Controller { get; set; } public string Action { get; set; } } 
  1. 假设我有一个单独的类来注册Web API路由(默认在Visual Studio ASP.NET MVC 4 Web应用程序项目的App_Start文件夹中创build):
  public static class WebApiConfig { public static void Register(HttpConfiguration config) { config.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{id}", defaults: new { id = RouteParameter.Optional } ); } } 
  1. 我可以轻松testing我的路线:
  [Test] public void GET_api_products_by_id_Should_route_to_ProductsController_Get_method() { // setups var request = new HttpRequestMessage(HttpMethod.Get, "http://myshop.com/api/products/1"); var config = new HttpConfiguration(); // act WebApiConfig.Register(config); var route = WebApi.RouteRequest(config, request); // asserts route.Controller.Should().Be<ProductsController>(); route.Action.Should().Be("Get"); } [Test] public void GET_api_products_Should_route_to_ProductsController_GetAll_method() { // setups var request = new HttpRequestMessage(HttpMethod.Get, "http://myshop.com/api/products"); var config = new HttpConfiguration(); // act WebApiConfig.Register(config); var route = WebApi.RouteRequest(config, request); // asserts route.Controller.Should().Be<ProductsController>(); route.Action.Should().Be("GetAll"); } .... 

下面的一些注释:

  • 是的,我使用绝对url。 但是在这里我没有看到任何问题,因为这些都是假的URL,我不需要为它们configuration任何工作,而且它们代表了真正的请求到我们的Web服务。
  • 如果在HttpConfiguration依赖关系(如上例所示)的独立类中configuration映射代码,则不需要复制路由映射代码到testing。
  • 在上面的例子中,我正在使用NUnit,NSubstitute和FluentAssertions,但是当然对于其他任何testing框架来说也是一件容易的事情。

ASP.NET Web API 2的一个迟到的答案(我只testing该版本)。 我使用了Nuget的MvcRouteTester.Mvc5,它为我做了这个工作。 你可以写下面的内容。

 [TestClass] public class RouteTests { private HttpConfiguration config; [TestInitialize] public void MakeRouteTable() { config = new HttpConfiguration(); WebApiConfig.Register(config); config.EnsureInitialized(); } [TestMethod] public void GetTest() { config.ShouldMap("/api/super") .To<superController>(HttpMethod.Get, x => x.Get()); } } 

我不得不在testing项目中添加nuget包Microsoft Asp.Net MVC 5.0.0版。 这不是太漂亮,但我没有find更好的解决scheme,这是我可以接受的。 您可以在nuget包pipe理器控制台中像这样安装旧版本:

Get-Project Tests | install-package microsoft.aspnet.mvc -version 5.0.0

它也适用于System.Web.Http.RouteAttribute。

此答案适用于WebAPI 2.0及以上版本

通过Whyleee的回答,我发现这个方法是基于一些潜在的脆弱的假设:

  1. 该方法尝试重新创build动作select,并假设Web API中的内部实现细节。
  2. 它假设当使用一个众所周知的公共可伸缩性点来replace它时,正在使用默认的控制器select器。

另一种方法是使用轻量级functiontesting。 这种方法的步骤是:

  1. 使用您的WebApiConfig.Register方法初始化testingHttpConfiguration对象,模仿应用程序在现实世界中被初始化的方式。
  2. 将自定义身份validation筛选器添加到捕获该级别的操作信息的testingconfiguration对象。 这可以通过开关直接在产品代码中注入或完成。 2.1validationfilter将短路任何filter以及动作代码,所以不必担心在动作方法本身中运行的实际代码。
  3. 使用内存中的服务器(HttpServer),并提出请求。 这是一个轻量级的方法,使用内存中的频道,所以它不会击中networking。
  4. 比较捕获的行动信息和预期的信息。
  [TestClass] public class ValuesControllerTest { [TestMethod] public void ActionSelection() { var config = new HttpConfiguration(); WebApiConfig.Register(config); Assert.IsTrue(ActionSelectorValidator.IsActionSelected( HttpMethod.Post, "http://localhost/api/values/", config, typeof(ValuesController), "Post")); } } 

这个帮助程序执行pipe道,并validation由authenticationfilter捕获的数据,其他属性也可以被捕获,或者可以通过在初始化时将lambda传递给filter来实现直接进行testing的客户filter。

  public class ActionSelectorValidator { public static bool IsActionSelected( HttpMethod method, string uri, HttpConfiguration config, Type controller, string actionName) { config.Filters.Add(new SelectedActionFilter()); HttpServer server = new HttpServer(config); HttpClient client = new HttpClient(server); HttpRequestMessage request = new HttpRequestMessage(method, uri); var response = client.SendAsync(request).Result; var actionDescriptor = (HttpActionDescriptor)response.RequestMessage.Properties["selected_action"]; return controller == actionDescriptor.ControllerDescriptor.ControllerType && actionName == actionDescriptor.ActionName; } } 

此filter将运行并阻止所有其他filter或操作代码的执行。

  public class SelectedActionFilter : IAuthenticationFilter { public Task AuthenticateAsync( HttpAuthenticationContext context, CancellationToken cancellationToken) { context.ErrorResult = CreateResult(context.ActionContext); // short circuit the rest of the authentication filters return Task.FromResult(0); } public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken) { var actionContext = context.ActionContext; actionContext.Request.Properties["selected_action"] = actionContext.ActionDescriptor; context.Result = CreateResult(actionContext); Assert.IsNull(context.Result); return Task.FromResult(0); } private static IHttpActionResult CreateResult( HttpActionContext actionContext) { var response = new HttpResponseMessage() { RequestMessage = actionContext.Request }; actionContext.Response = response; return new ByPassActionResult(response); } public bool AllowMultiple { get { return true; } } } 

结果会缩短执行时间

  internal class ByPassActionResult : IHttpActionResult { public HttpResponseMessage Message { get; set; } public ByPassActionResult(HttpResponseMessage message) { Message = message; } public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken) { return Task.FromResult<HttpResponseMessage>(Message); } } 

感谢上面的答案whyleee!

我已经将它与一些我喜欢从WebApiContrib.Testing库中语法化的元素结合起来,而这些元素并不适合我生成以下的辅助类。

这允许我写这样真正轻量级的testing…

 [Test] [Category("Auth Api Tests")] public void TheAuthControllerAcceptsASingleItemGetRouteWithAHashString() { "http://api.siansplan.com/auth/sjkfhiuehfkshjksdfh".ShouldMapTo<AuthController>("Get", "hash"); } [Test] [Category("Auth Api Tests")] public void TheAuthControllerAcceptsAPost() { "http://api.siansplan.com/auth".ShouldMapTo<AuthController>("Post", HttpMethod.Post); } 

我还稍微增强了它,以便在需要时testing参数(这是一个params数组,因此您可以添加所有你喜欢的东西,而且只是检查它们是否存在)。 这也已经改编成最小起订量,纯粹是因为它是我的select框架…

 using Moq; using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.Dispatcher; using System.Web.Http.Hosting; using System.Web.Http.Routing; namespace SiansPlan.Api.Tests.Helpers { public static class RoutingTestHelper { /// <summary> /// Routes the request. /// </summary> /// <param name="config">The config.</param> /// <param name="request">The request.</param> /// <returns>Inbformation about the route.</returns> public static RouteInfo RouteRequest(HttpConfiguration config, HttpRequestMessage request) { // create context var controllerContext = new HttpControllerContext(config, new Mock<IHttpRouteData>().Object, request); // get route data var routeData = config.Routes.GetRouteData(request); RemoveOptionalRoutingParameters(routeData.Values); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; controllerContext.RouteData = routeData; // get controller type var controllerDescriptor = new DefaultHttpControllerSelector(config).SelectController(request); controllerContext.ControllerDescriptor = controllerDescriptor; // get action name var actionMapping = new ApiControllerActionSelector().SelectAction(controllerContext); var info = new RouteInfo(controllerDescriptor.ControllerType, actionMapping.ActionName); foreach (var param in actionMapping.GetParameters()) { info.Parameters.Add(param.ParameterName); } return info; } #region | Extensions | /// <summary> /// Determines that a URL maps to a specified controller. /// </summary> /// <typeparam name="TController">The type of the controller.</typeparam> /// <param name="fullDummyUrl">The full dummy URL.</param> /// <param name="action">The action.</param> /// <param name="parameterNames">The parameter names.</param> /// <returns></returns> public static bool ShouldMapTo<TController>(this string fullDummyUrl, string action, params string[] parameterNames) { return ShouldMapTo<TController>(fullDummyUrl, action, HttpMethod.Get, parameterNames); } /// <summary> /// Determines that a URL maps to a specified controller. /// </summary> /// <typeparam name="TController">The type of the controller.</typeparam> /// <param name="fullDummyUrl">The full dummy URL.</param> /// <param name="action">The action.</param> /// <param name="httpMethod">The HTTP method.</param> /// <param name="parameterNames">The parameter names.</param> /// <returns></returns> /// <exception cref="System.Exception"></exception> public static bool ShouldMapTo<TController>(this string fullDummyUrl, string action, HttpMethod httpMethod, params string[] parameterNames) { var request = new HttpRequestMessage(httpMethod, fullDummyUrl); var config = new HttpConfiguration(); WebApiConfig.Register(config); var route = RouteRequest(config, request); var controllerName = typeof(TController).Name; if (route.Controller.Name != controllerName) throw new Exception(String.Format("The specified route '{0}' does not match the expected controller '{1}'", fullDummyUrl, controllerName)); if (route.Action.ToLowerInvariant() != action.ToLowerInvariant()) throw new Exception(String.Format("The specified route '{0}' does not match the expected action '{1}'", fullDummyUrl, action)); if (parameterNames.Any()) { if (route.Parameters.Count != parameterNames.Count()) throw new Exception( String.Format( "The specified route '{0}' does not have the expected number of parameters - expected '{1}' but was '{2}'", fullDummyUrl, parameterNames.Count(), route.Parameters.Count)); foreach (var param in parameterNames) { if (!route.Parameters.Contains(param)) throw new Exception( String.Format("The specified route '{0}' does not contain the expected parameter '{1}'", fullDummyUrl, param)); } } return true; } #endregion #region | Private Methods | /// <summary> /// Removes the optional routing parameters. /// </summary> /// <param name="routeValues">The route values.</param> private static void RemoveOptionalRoutingParameters(IDictionary<string, object> routeValues) { var optionalParams = routeValues .Where(x => x.Value == RouteParameter.Optional) .Select(x => x.Key) .ToList(); foreach (var key in optionalParams) { routeValues.Remove(key); } } #endregion } /// <summary> /// Route information /// </summary> public class RouteInfo { #region | Construction | /// <summary> /// Initializes a new instance of the <see cref="RouteInfo"/> class. /// </summary> /// <param name="controller">The controller.</param> /// <param name="action">The action.</param> public RouteInfo(Type controller, string action) { Controller = controller; Action = action; Parameters = new List<string>(); } #endregion public Type Controller { get; private set; } public string Action { get; private set; } public List<string> Parameters { get; private set; } } } 

我已经采取了基思jackson的解决scheme,并将其修改为:

a)使用asp.net web api 2 – 属性路由以及旧学校路由

b)不仅要validation路线参数名称,还要validation它们的值

例如以下路线

  [HttpPost] [Route("login")] public HttpResponseMessage Login(string username, string password) { ... } [HttpPost] [Route("login/{username}/{password}")] public HttpResponseMessage LoginWithDetails(string username, string password) { ... } 

您可以validation路由匹配正确的http方法,控制器,操作和参数:

  [TestMethod] public void Verify_Routing_Rules() { "http://api.appname.com/account/login" .ShouldMapTo<AccountController>("Login", HttpMethod.Post); "http://api.appname.com/account/login/ben/password" .ShouldMapTo<AccountController>( "LoginWithDetails", HttpMethod.Post, new Dictionary<string, object> { { "username", "ben" }, { "password", "password" } }); } 

基思·jackson修改whyleee的解决scheme的修改。

  public static class RoutingTestHelper { /// <summary> /// Routes the request. /// </summary> /// <param name="config">The config.</param> /// <param name="request">The request.</param> /// <returns>Inbformation about the route.</returns> public static RouteInfo RouteRequest(HttpConfiguration config, HttpRequestMessage request) { // create context var controllerContext = new HttpControllerContext(config, new Mock<IHttpRouteData>().Object, request); // get route data var routeData = config.Routes.GetRouteData(request); RemoveOptionalRoutingParameters(routeData.Values); HttpActionDescriptor actionDescriptor = null; HttpControllerDescriptor controllerDescriptor = null; // Handle web api 2 attribute routes if (routeData.Values.ContainsKey("MS_SubRoutes")) { var subroutes = (IEnumerable<IHttpRouteData>)routeData.Values["MS_SubRoutes"]; routeData = subroutes.First(); actionDescriptor = ((HttpActionDescriptor[])routeData.Route.DataTokens.First(token => token.Key == "actions").Value).First(); controllerDescriptor = actionDescriptor.ControllerDescriptor; } else { request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; controllerContext.RouteData = routeData; // get controller type controllerDescriptor = new DefaultHttpControllerSelector(config).SelectController(request); controllerContext.ControllerDescriptor = controllerDescriptor; // get action name actionDescriptor = new ApiControllerActionSelector().SelectAction(controllerContext); } return new RouteInfo { Controller = controllerDescriptor.ControllerType, Action = actionDescriptor.ActionName, RouteData = routeData }; } #region | Extensions | public static bool ShouldMapTo<TController>(this string fullDummyUrl, string action, Dictionary<string, object> parameters = null) { return ShouldMapTo<TController>(fullDummyUrl, action, HttpMethod.Get, parameters); } public static bool ShouldMapTo<TController>(this string fullDummyUrl, string action, HttpMethod httpMethod, Dictionary<string, object> parameters = null) { var request = new HttpRequestMessage(httpMethod, fullDummyUrl); var config = new HttpConfiguration(); WebApiConfig.Register(config); config.EnsureInitialized(); var route = RouteRequest(config, request); var controllerName = typeof(TController).Name; if (route.Controller.Name != controllerName) throw new Exception(String.Format("The specified route '{0}' does not match the expected controller '{1}'", fullDummyUrl, controllerName)); if (route.Action.ToLowerInvariant() != action.ToLowerInvariant()) throw new Exception(String.Format("The specified route '{0}' does not match the expected action '{1}'", fullDummyUrl, action)); if (parameters != null && parameters.Any()) { foreach (var param in parameters) { if (route.RouteData.Values.All(kvp => kvp.Key != param.Key)) throw new Exception(String.Format("The specified route '{0}' does not contain the expected parameter '{1}'", fullDummyUrl, param)); if (!route.RouteData.Values[param.Key].Equals(param.Value)) throw new Exception(String.Format("The specified route '{0}' with parameter '{1}' and value '{2}' does not equal does not match supplied value of '{3}'", fullDummyUrl, param.Key, route.RouteData.Values[param.Key], param.Value)); } } return true; } #endregion #region | Private Methods | /// <summary> /// Removes the optional routing parameters. /// </summary> /// <param name="routeValues">The route values.</param> private static void RemoveOptionalRoutingParameters(IDictionary<string, object> routeValues) { var optionalParams = routeValues .Where(x => x.Value == RouteParameter.Optional) .Select(x => x.Key) .ToList(); foreach (var key in optionalParams) { routeValues.Remove(key); } } #endregion } /// <summary> /// Route information /// </summary> public class RouteInfo { public Type Controller { get; set; } public string Action { get; set; } public IHttpRouteData RouteData { get; set; } } 

所有其他的答案因为我无法弄清楚的一些细节而失败了。

下面是使用GetRouteData()的完整示例: https : //github.com/JayBazuzi/ASP.NET-WebApi-GetRouteData-example ,创build如下:

  1. 在VS 2013中,新build项目 – > Web,ASP.NET Web应用程序
  2. selectWebAPI。 选中“添加unit testing”。
  3. 添加以下unit testing:

     [TestMethod] public void RouteToGetUser() { var request = new HttpRequestMessage(HttpMethod.Get, "http://localhost:4567/api/users/me"); var config = new HttpConfiguration(); WebApiConfig.Register(config); config.EnsureInitialized(); var result = config.Routes.GetRouteData(request); Assert.AreEqual("api/{controller}/{id}", result.Route.RouteTemplate); } 

为了从路由集合中获取路由数据,在这种情况下你需要提供一个完整的URI(只需使用“http:// localhost / api / super”)。

要testing从RouteTable.Routes的路线,你可能会做这样的事情:

 var httpConfig = GlobalConfiguration.Configuration; httpConfig.Routes.MapHttpRoute("DefaultApi", "api/{controller}/"); 

封面上发生的事情是GlobalConfiguration将使RouteTable.Routes适应httpConfig.Routes。 所以当你添加一个路由到httpConfig.Routes,它实际上被添加到RouteTable.Routes。 但是为了实现这个function,您需要在ASP.NET中进行托pipe,以便环境设置(例如HostingEnvironment.ApplicationVirtualPath)被填充。