39 votes

Test de la configuration de la route dans ASP.NET WebApi

Je suis en train de faire quelques tests unitaires de mon WebApi route de configuration. Je veux tester que l'itinéraire "/api/super" cartes à l' Get() méthode de mon SuperController. J'ai configuré le test ci-dessous et je rencontre quelques problèmes.

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

Mon premier problème est que si je n'ai pas spécifier une URL complète httpConfig.Routes.GetRouteData(request); jette un InvalidOperationException d'exception avec un message de "Cette opération n'est pas prise en charge pour un URI relatif."

Je suis évidemment manquer quelque chose avec mon écrasé de configuration. Je préfère utiliser un URI relatif car il ne semble pas raisonnable d'utiliser un spécialiste de URI pour la route test.

Mon deuxième problème avec ma configuration ci-dessus est que je ne suis pas tester mes itinéraires comme configuré dans mon RouteConfig mais je suis plutôt à l'aide de:

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

Comment puis-je faire usage de la attribué RouteTable.Routes tel que configuré dans un typique Mondiale.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
    }
}

Ce que j'ai écrasé ci-dessus peuvent ne pas être la meilleure configuration de test. Si il y a une approche plus rationnelle, je suis toutes les oreilles.

25voto

whyleee Points 1954

Je viens de tester mon API Web routes, et voici comment je l'ai fait.

  1. Tout d'abord, j'ai créé une aide pour déplacer toutes les API Web de routage logique:
    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. En supposant que j'ai une catégorie distincte pour enregistrer l'API Web de routes (il est créé par défaut dans Visual Studio ASP.NET MVC 4 projet d'Application Web, dans le App_Start dossier):
    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. Je peux tester mes itinéraires facilement:
    [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");
    }

    ....

Quelques notes ci-dessous:

  • Oui, je suis en utilisant des Url absolues. Mais je ne vois pas de problèmes ici, parce que ce sont des faux Url, je n'ai pas besoin de configurer quoi que ce soit pour leur travail, et ils représentent réel les demandes de nos services web.
  • Vous n'avez pas besoin de copier le trajet de mappages de code pour les tests, s'ils sont configurés dans la classe séparée avec HttpConfiguration de dépendance (comme dans l'exemple ci-dessus).
  • Je suis l'aide de NUnit, NSubstitute et FluentAssertions dans l'exemple ci-dessus, mais bien sûr, c'est une tâche facile à faire de même avec toutes les autres frameworks de test.

13voto

Skuli Points 547

Une réponse tardive pour ASP.NET Web API 2 ( j'ai testé uniquement pour cette version ). J'ai utilisé MvcRouteTester.Mvc5 de Nuget et il fait le travail pour moi. vous pouvez écrire ce qui suit.

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

J'ai dû ajouter nuget package Microsoft Asp.Net MVC version 5.0.0 pour le projet de test. Ce n'est pas trop jolie mais je n'ai pas trouvé une meilleure solution et c'est acceptable pour moi. Vous pouvez installer l'ancienne version comme ça dans le gestionnaire de package nuget console:

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

Il fonctionne avec le Système.Web.Http.RouteAttribute trop.

9voto

Yishai Galatzer Points 2465

** À noter que cela a été testé avec l'API Web 2.0

De la lecture à travers Whyleee réponse, j'ai remarqué que l'approche est basée sur un couple d'assez forts et potentiellement fragiles hypothèses:

  1. L'approche essaie de recréer la sélection de l'action, et assume l'intérieur les détails d'implémentation de l'API Web.
  2. Il suppose que le contrôleur par défaut sélecteur est utilisé, quand il y a un public bien connu extensiblity point qui permet de le remplacer.

Une approche alternative est d'utiliser un test d'intégration. Les étapes de cette démarche sont:

  1. Initialiser un test HttpConfiguration objet à l'aide de votre WebApiConfig.Méthode Register, imitant la façon dont l'application serait initialisé dans un monde réel. Mise à jour 1/10 - Utiliser l'authentification filtre plutôt que sur l'action du filtre
  2. Ajouter un filtre d'authentification pour le test de configuration de l'objet qui capte l'action d'information à ce niveau. 2.1 Le filtre d'authentification sera/peut court-circuiter l'action ainsi que d'autres filtres, donc il n'y a aucun soucis avec le code en cours d'exécution dans la méthode de l'action elle-même.
  3. L'utilisation de la mémoire du serveur (serveur http), et de faire une demande. C'est en fait assez léger, et envoie rien sur le fil.
  4. Comparer les saisies d'une action d'information avec les informations attendues.

    [TestClass]
    public class ValuesControllerTest
    {
        [TestMethod]
        public void ActionSelection()
        {
            HttpConfiguration config = new HttpConfiguration();
            WebApiConfig.Register(config);
            Assert.IsTrue(IsActionSelected(
                HttpMethod.Post,
                "http://localhost/api/values/",
                config,
                typeof(ValuesController),
                "Post"));
        }
    
        public 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);
            HttpResponseMessage response = client.SendAsync(request).Result;
            HttpActionDescriptor 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)
        {
            HttpActionContext actionContext = context.ActionContext;
            actionContext.Request.Properties["selected_action"] = actionContext.ActionDescriptor;
    
            context.Result = CreateResult(actionContext); // short circuit the rest of the pipline
    
            Assert.IsNull(context.Result);
            return Task.FromResult(0);
        }
    
        private static IHttpActionResult CreateResult(HttpActionContext actionContext)
        {
            HttpResponseMessage response = new HttpResponseMessage() { RequestMessage = actionContext.Request };
            actionContext.Response = response;
    
            return new ByPassActionResult(response);
        }
    
        private 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);
            }
        }
    
        public bool AllowMultiple
        {
            get { return true; }
        }
    }
    

5voto

Ben Priebe Points 131

J'ai pris Keith Jackson solution et l'a modifié pour:

a) travailler avec asp.net web api 2 - l'attribut de routage ainsi que de la vieille école de routage

et

b) vérifier non seulement la route des noms de paramètres, mais aussi de leurs valeurs
 

par exemple, pour les itinéraires suivants

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


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

Vous pouvez vérifier les itinéraires de choisir la bonne méthode http, le contrôleur, l'action et les paramètres:

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

Modifications de Keith Jackson modifications whyleee de la solution.

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

3voto

Keith Jackson Points 339

Grâce à whyleee pour la réponse ci-dessus!

Je l'ai combiné avec quelques-uns des éléments que j'ai aimé du point de vue syntaxique de la WebApiContrib.Bibliothèque de test, qui n'a pas de travail pour moi pour générer les éléments suivants de la classe helper.

Cela me permet d'écrire vraiment léger des tests de ce genre...

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

J'ai également amélioré légèrement pour permettre de tester les paramètres en cas de besoin (c'est un tableau params de sorte que vous pouvez ajouter tout ce que vous voulez et il vérifie juste qu'ils sont présents). Cela a également été adapté pour MOQ, purement comme c'est mon cadre de choix...

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

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X