在ASP.NET WebApi中测试路由配置

ahs*_*ele 39 unit-testing routes asp.net-web-api

我正在尝试对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");
}
Run Code Online (Sandbox Code Playgroud)

我的第一个问题是,如果我没有指定一个完全限定的URL,则httpConfig.Routes.GetRouteData(request);抛出一个InvalidOperationException异常,并显示"相对URI不支持此操作"的消息.

我很明显错过了我的存根配置.我更喜欢使用相对URI,因为使用完全限定的URI进行路由测试似乎不合理.

我上面的配置的第二个问题是我没有按照RouteConfig中的配置测试我的路由,而是使用:

var routeCollection = new HttpRouteCollection();
routeCollection.MapHttpRoute("DefaultApi", "api/{controller}/");
Run Code Online (Sandbox Code Playgroud)

如何使用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
    }
}
Run Code Online (Sandbox Code Playgroud)

我在上面提到的内容可能不是最好的测试配置.如果有一种更简化的方法,我全都耳朵.

why*_*eee 25

我最近在测试我的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; }
    }
Run Code Online (Sandbox Code Playgroud)
  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 }
            );
        }
    }
Run Code Online (Sandbox Code Playgroud)
  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");
    }

    ....
Run Code Online (Sandbox Code Playgroud)

以下注意事项:

  • 是的,我正在使用绝对URL.但是我在这里看不到任何问题,因为这些是伪造的URL,我不需要为它们配置任何工作,它们代表对我们的Web服务的实际请求.
  • 如果在具有HttpConfiguration依赖关系的单独类中配置了路由映射代码,则不需要将路由映射代码复制到测试中(如上例所示).
  • 我在上面的示例中使用了NUnit,NSubstitute和FluentAssertions,但当然,对任何其他测试框架执行相同操作也是一件容易的事.

  • routeData将为null,因为config.Routes.GetRouteData(request)将为null.这会导致您使用不正确的路由检查的异常.我们也不应该有if(routeData == null)抛出新的HttpException(404,"Route"未找到 "); 并断言例外.只是添加我今天经历的tid位... (2认同)
  • 太好了!不过,我同意@ashutoshraina的无效检查.是否有理由通过RemoveOptionalRoutingParameter清除routeData.Values?它们对于断言传入的参数值很有用. (2认同)
  • 这种方法是否适用于属性路由?它似乎对内部细节做了一些假设.我认为运行更多的管道(如Yishai的方法)将不那么脆弱. (2认同)

Sku*_*uli 13

ASP.NET Web API 2的最新答案(我只测试了该版本).我使用了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());
    }
}
Run Code Online (Sandbox Code Playgroud)

我不得不将nuget包Microsoft Asp.Net MVC版本5.0.0添加到测试项目中.这不是太漂亮,但我找不到更好的解决方案,这对我来说是可以接受的.您可以在nuget包管理器控制台中安装这样的旧版本:

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

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


Yis*_*zer 9

此答案适用于WebAPI 2.0及更高版本

通过Whyleee的回答,我注意到这种方法是基于耦合和脆弱的假设:

  1. 该方法尝试重新创建操作选择,并假定Web API中的内部实现细节.
  2. 它假设正在使用默认控制器选择器,当有一个众所周知的公共扩展点允许替换它时.

另一种方法是使用轻量级功能测试.这种方法的步骤是:

  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"));
    }
 }
Run Code Online (Sandbox Code Playgroud)

该帮助程序执行管道,并验证由身份验证过滤器捕获的数据,也可以捕获其他属性,或者可以实现客户过滤器,通过在初始化时将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;
    }
}
Run Code Online (Sandbox Code Playgroud)

此过滤器运行并阻止过滤器或操作代码的所有其他执行.

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; } }
}
Run Code Online (Sandbox Code Playgroud)

结果会使执行短路

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);
    }
}
Run Code Online (Sandbox Code Playgroud)


Ben*_*ebe 5

我采用了Keith Jackson的解决方案,并将其修改为:

a)使用asp.net Web 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)
    {
        ...
    }
Run Code Online (Sandbox Code Playgroud)

  您可以验证路由是否与正确的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" } 
               });
    }
Run Code Online (Sandbox Code Playgroud)

  基思·杰克逊(Keith Jackson)对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; }
    }
Run Code Online (Sandbox Code Playgroud)