Подтвердить что ты не робот

Тестирование конфигурации маршрута в ASP.NET WebApi

Я пытаюсь выполнить частичное тестирование конфигурации маршрута 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
    }
}

Дальше то, что я пропустил выше, может быть не лучшей тестовой конфигурацией. Если есть более оптимизированный подход, я все уши.

4b9b3361

Ответ 1

Недавно я тестировал маршруты веб-API, и вот как я это сделал.

  • Во-первых, я создал помощника для перемещения всей логики маршрутизации 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; }
    }
  • Предполагая, что у меня есть отдельный класс для регистрации маршрутов веб-API (он создается по умолчанию в проекте веб-приложения Visual Studio ASP.NET MVC 4 в папке 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 }
            );
        }
    }
  • Я могу легко проверить свои маршруты:
    [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-адреса, мне не нужно ничего настраивать для их работы, и они представляют реальные запросы к нашим веб-службам.
  • Вам не нужно копировать код сопоставления маршрута в тесты, если они настроены в отдельном классе с зависимостью HttpConfiguration (как в примере выше).
  • В приведенном выше примере я использую NUnit, NSubstitute и FluentAssertions, но, конечно, это простая задача сделать то же самое с любыми другими тестовыми платформами.

Ответ 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());
    }
}

Мне пришлось добавить в тестовый проект пакет 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.

Ответ 3

Этот ответ действителен для WebAPI 2.0 и выше

Чтение через ответ Whyleee, я заметил, что подход основан на парапотенциально хрупких предположениях:

  • Подход пытается воссоздать выбор действия и предполагает внутренние детали реализации в Web API.
  • Предполагается, что используется селектор контроллера по умолчанию, когда имеется известная точка публичной расширяемости, которая позволяет ее заменить.

Альтернативный подход заключается в использовании легкого функционального теста. Шагами в этом подходе являются:

  • Инициализировать тестовый объект HttpConfiguration с помощью метода WebApiConfig.Register, отображая способ инициализации приложения в реальном мире.
  • Добавьте настраиваемый фильтр проверки подлинности к объекту тестовой конфигурации, который захватывает информацию о действиях на этом уровне. Это можно ввести или сделать непосредственно в коде продукта с помощью переключателя. 2.1. Фильтр аутентификации будет закоротить любые фильтры, а также код действия, поэтому нет никаких проблем с фактическим кодом, выполняемым в самом методе действий.
  • Используйте сервер in-memory (HttpServer) и выполните запрос. Это легкий подход, используя канал в памяти, поэтому он не попадет в сеть.
  • Сравните полученную информацию о действии с ожидаемой информацией.
    [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"));
        }
     }

Этот помощник выполняет конвейер и проверяет данные, фильтр аутентификации, другие свойства также могут быть захвачены OR может быть реализован клиентский фильтр, который выполняет валидацию непосредственно для каждого теста, передавая лямбда в фильтр при инициализации.

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

Этот фильтр запускает и блокирует все другие исполнения фильтров или кода действия.

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

Ответ 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; }
    }
}

Ответ 5

Я взял решение Кейта Джексона и изменил его на:

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)
    {
        ...
    }

  Вы можете проверить соответствие маршрутов правильному методу 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; }
    }

Ответ 6

Все другие ответы мне не удалось из-за некоторых деталей, которые я не мог понять.

Вот полный пример использования GetRouteData(): https://github.com/JayBazuzi/ASP.NET-WebApi-GetRouteData-example, созданный следующим образом:

  • В VS 2013, Новый Проект → Веб, Веб-приложение ASP.NET
  • Выберите WebAPI. Проверьте "Добавить модульные тесты".
  • Добавьте следующий unit test:

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

Ответ 7

Чтобы получить данные маршрута из коллекций маршрутов, в этом случае вам необходимо предоставить полный URI (просто используйте "http://localhost/api/super" ).

Для тестирования маршрутов из 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.