测试路由配置在ASP.NET的WebAPI测试路由配置在ASP.NET的WebAPI(Testing

2019-05-12 23:08发布

我试图做我的一些单元测试的WebAPI路由配置。 我想测试的路线"/api/super"映射到Get()我的方法SuperController 。 我设置了以下测试时遇到的几个问题。

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); 抛出一个InvalidOperationException与消息异常“不支持相对URI此操作”。

我显然缺少与我的存根配置的东西。 我宁愿使用相对URI作为它似乎并没有合理的使用完全合格的URI路径测试。

我与我上面的配置第二个问题,作为配置在我RouteConfig但我不使用我不测试我的路线:

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

如何利用分配的RouteTable.Routes作为一个典型的Global.asax配置:

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
    }
}

而且上面我所掐灭可能不是最好的测试配置。 如果有一个更精简的方法,我洗耳恭听。

Answer 1:

我最近测试我的Web API的路线,这里是我是怎么做的。

  1. 首先,我创建了一个帮手,有移动所有的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文件夹中):
    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. 我可以很容易地测试我的路线:
    [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。 但我看不出有任何的问题在这里,因为这些都是假的网址,我不需要配置任何东西为他们工作,他们代表我们的网络服务,实时请求。
  • 你并不需要你的路由映射代码复制到测试,如果他们在与HttpConfiguration依赖单独的类配置(如上面的例子)。
  • 我使用NUnit,NSubstitute和FluentAssertions在上面的例子,但当然是一件容易的事做同样与任何其他测试框架。


Answer 2:

逾期答案的ASP.NET Web API 2(我只测试该版本)。 我用MvcRouteTester.Mvc5从的NuGet和它的工作对我来说。 你可以写出如下。

[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());
    }
}

我只好包微软Asp.Net MVC版本5.0.0添加的NuGet的测试项目。 这不是太漂亮,但我没有找到一个更好的解决方案,这是可以接受我。 您可以安装旧版本像这样的NuGet包管理器控制台:

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

它的工作原理与System.Web.Http.RouteAttribute了。



Answer 3:

这个答案是适用于上述的WebAPI 2.0和

通过Whyleee的答案阅读,我注意到,该方法是基于耦合和脆弱的假设:

  1. 该方法试图重建行动的选择,并在网页API假定内部实现细节。
  2. 它假定默认控制器选择器正在被使用,当存在允许替换它公知的公共extensiblity点。

另一种方法是使用重量轻的功能测试。 这种方法的步骤如下:

  1. 初始化使用WebApiConfig.Register方法试验HttpConfiguration对象,模仿的应用程序将在一个真实的世界被初始化的方式。
  2. 自定义的认证过滤器添加到捕获处于该级别的动作信息的测试配置对象。 这可以被注射或通过开关在产品代码直接完成。 2.1认证滤波器将短路的任何过滤器,以及操作代码,所以存在与实际的代码没有被关注的操作方法本身运行。
  3. 使用内存的服务器(的HttpServer),并发出请求。 这种方法使用一个内存通道,这样就不会打网络。
  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"));
    }
 }

这个辅助执行管线,并验证由认证过滤器捕获的数据,其他属性可以被捕获以及或客户过滤器可以实现直接做验证每个测试,通过传递一个lambda入过滤器上初始化。

 public class ActionSelectorValidator
 {
    public static bool IsActionSelected(
        HttpMethod method,
        string uri,
        HttpConfiguration config,
        Type controller,
        string actionName)
    {
        config.Filters.Add(new SelectedActionFilter());
        var server = new HttpServer(config);
        var client = new HttpClient(server);
        var 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;
    }
}

该过滤器运行和块的过滤器或操作代码所有其他的执行。

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


        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);
    }
}


Answer 4:

由于whyleee以上答案!

我曾与一些我从WebApiContrib.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);
}

我也增强了它稍微允许在需要时参数测试(这是一个参数数组,所以你可以添加你喜欢的,它只是检查它们存在)。 这也被改编为MOQ,纯粹是因为它是我选择的框架...

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; }
    }
}


Answer 5:

我已经采取了基思·杰克逊的解决方案,并将其修改为:

属性路由以及老同学路由- a)用asp.net网页API 2工作

B)不仅验证路径参数的名称也是它们的值

如以下路线

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


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

您可以验证路由匹配正确的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" } 
               });
    }

修改基思·杰克逊的修改whyleee的解决方案。

    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; }
    }


Answer 6:

一切缘于一些细节我无法找出失败对我其他的答案。

下面是一个使用一个完整的示例GetRouteData() https://github.com/JayBazuzi/ASP.NET-WebApi-GetRouteData-example ,这样创建的:

  1. 在VS 2013年,新建项目 - >网页,ASP.NET Web应用程序
  2. 选择的WebAPI。 选中“添加单元测试”。
  3. 添加以下的单元测试:

     [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); } 


Answer 7:

为了从路线藏品的路线数据,你需要在这种情况下,提供了完整的URI(只使用“http://本地主机/ API /超”)。

要测试从RouteTable.Routes的路线,你有可能做这样的事情:

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

这是怎么回事在幕后是,GlobalConfiguration将适应RouteTable.Routes到httpConfig.Routes。 所以,当你添加路由到httpConfig.Routes,它实际上被添加到RouteTable.Routes。 但是,对于这个工作,你需要内部ASP.NET进行托管,因此,环境设置,如HostingEnvironment.ApplicationVirtualPath被填充。



文章来源: Testing route configuration in ASP.NET WebApi