J'essaie de faire des tests unitaires de ma configuration de route WebApi . Je veux tester que la route "/api/super"
Correspond à la méthode Get()
de mon SuperController
. J'ai configuré le test ci-dessous et j'ai 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 ne spécifie pas une URL complète, httpConfig.Routes.GetRouteData(request);
lève une exception InvalidOperationException
avec un message "Cette opération n'est pas prise en charge pour un URI relatif".
Il me manque évidemment quelque chose avec ma configuration tronquée. Je préférerais utiliser un URI relatif car il ne semble pas raisonnable d'utiliser un URI pleinement qualifié pour les tests de route.
Mon deuxième problème avec ma configuration ci-dessus est que je ne teste pas mes itinéraires tels que configurés dans mon RouteConfig mais j'utilise plutôt:
var routeCollection = new HttpRouteCollection();
routeCollection.MapHttpRoute("DefaultApi", "api/{controller}/");
Comment puis-je utiliser le RouteTable.Routes
Attribué tel qu'il est configuré dans un Global.asax typique:
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
}
}
De plus, ce que j'ai décrit ci-dessus n'est peut-être pas la meilleure configuration de test. S'il y a une approche plus rationalisée, je suis tous à l'écoute.
Je testais récemment mes routes d'API Web, et voici comment je l'ai fait.
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; }
}
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");
}
....
Quelques notes ci-dessous:
Une réponse tardive pour ASP.NET Web API 2 (je n'ai testé que 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 le package nuget Microsoft Asp.Net MVC version 5.0.0 au projet de test. Ce n'est pas trop joli mais je n'ai pas trouvé de meilleure solution et c'est acceptable pour moi. Vous pouvez installer l'ancienne version comme celle-ci dans la console du gestionnaire de paquets nuget:
Get-Project Tests | install-package Microsoft.aspnet.mvc -version 5.0.0
Il fonctionne également avec System.Web.Http.RouteAttribute.
Cette réponse est valable pour WebAPI 2.0 et supérieur
En lisant la réponse de Whyleee, j'ai remarqué que l'approche est basée sur des hypothèses couplées et fragiles:
Une autre approche consiste à utiliser un test fonctionnel léger. Les étapes de cette approche sont les suivantes:
[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"));
}
}
Cet assistant exécute le pipeline et valide les données capturées par le filtre d'authentification, d'autres propriétés peuvent également être capturées OR un filtre client peut être implémenté qui effectue la validation directement par test, en passant un lambda dans le filtre lors de l'initialisation.
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;
}
}
Ce filtre s'exécute et bloque toutes les autres exécutions de filtres ou de code d'action.
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; } }
}
Un résultat qui court-circuitera l'exécution
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);
}
}
J'ai pris la solution de Keith Jackson et l'ai modifiée pour:
a) travailler avec asp.net web api 2 - routage d'attributs ainsi comme routage old school
et
b) vérifier non seulement les noms des paramètres de route mais aussi 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 que les itinéraires correspondent à la méthode http, au contrôleur, à l'action et aux paramètres corrects:
[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 apportées aux modifications de Keith Jackson à la solution de 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; }
}
Merci à whyleee pour la réponse ci-dessus!
Je l'ai combiné avec certains des éléments que j'aimais syntaxiquement à partir de la bibliothèque WebApiContrib.Testing, qui ne fonctionnait pas pour moi pour générer la classe d'assistance suivante.
Cela me permet d'écrire des tests vraiment légers comme celui-ci ...
[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);
}
Je l'ai également légèrement amélioré pour permettre de tester les paramètres en cas de besoin (il s'agit d'un tableau de paramètres afin que vous puissiez ajouter tout ce que vous aimez et il vérifie simplement qu'ils sont présents). Cela a également été adapté pour MOQ, car 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; }
}
}
Toutes les autres réponses ont échoué pour moi en raison de certains détails que je n'ai pas pu comprendre.
Voici un exemple complet d'utilisation de GetRouteData()
: https://github.com/JayBazuzi/ASP.NET-WebApi-GetRouteData-example , créé comme ceci:
Ajoutez le test unitaire suivant:
[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);
}