W poprzednim wpisie zajęliśmy się testowaniem routingu w WebAPI. Napisany kod był dość brzydki i warto go po prostu umieścić w osobnej klasie, tak abyśmy mogli z niego korzystać w różnych testach.
Zaznaczam, że wciąż nie jest to kod produkcyjny. Zdecydowanie nie będzie pokrywał wszystkich scenariuszy, dlatego odradzam umieszczenie go w swoim wewnętrznym repozytorium NuGet. Z drugiej jednak strony, nic nie szkodzi na przeszkodzie, aby korzystać z niego konkretnym projekcie i w razie potrzeby naprawić jego usterki.
Na GitHub znajduje się już inna biblioteka WebAPIContrib, gdzie można skorzystać z bardzo dobrej implementacji:
Niestety ma ona referencje do zbyt wielu bibliotek, dlatego zdecydowałem napisać się własny mini-helper, który jest wzorowany na powyższym kodzie. Jeśli ktoś z Was chce użyć kodu w produkcji, zdecydowanie polecam przyjrzenie się WebApiContrig i potraktowanie mojego wpisu bardziej jako przewodnika a nie implementacji z której można korzystać (poniższy kod powstał w 10 minut…).
Tak jak w poprzednim poście, załóżmy, że mamy następujący kontroler:
public class SampleController : ApiController { [Route("site/{siteId}/person/{personid}/profile/words/{searchText}")] public int[] GetData(int siteId, int personId, string searchText) { return Enumerable.Range(1, 10).ToArray(); } }
Zamiast kopiować kod z poprzedniego wpisu za każdym razem jak chcemy przetestować routing, chcemy użyć po prostu wyrażenia lambda np.:
const string url = "http://www.test.com/site/1/person/3/profile/words/ppp"; RoutingAssert.Verify<SampleController>(url, x => x.GetData(1, 3, "ppp"));
Pierwszy parametr to oczywiście URL a drugi to wyrażenie z którego możemy odczytać następujące informacje:
- Typ kontrolera
- Nazwę akcji
- Typ i wartości przekazanych parametrów.
Nie trudno domyślić się, że największym wyzwaniem będzie odczytanie tych wartości z wyrażenia – reszta pozostanie taka sama jak w poprzednim wpisie.
Zaczniemy zatem od następującej sygnatury:
public class RoutingAssert { public static void Verify<T>(string url, Expression<Action<T>> mapping) { } }
Z poprzedniego wpisu pamiętamy, że potrzebujemy zainicjalizować HttpConfiguration:
private static HttpConfiguration InitConfig() { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.EnsureInitialized(); return config; }
W tej chwili, inicjalizujemy wyłącznie routing za pomocą atrybutów.
Kolejny etap to stworzenie HTTPRequest:
private static HttpRequestMessage InitRequest(string url, HttpConfiguration config) { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); var routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; return request; }
Następnie za pomocą selektora, możemy uzyskać deskryptor kontrolera:
private HttpControllerDescriptor GetControllerDescriptor(HttpConfiguration config, HttpRequestMessage request) { IHttpControllerSelector controllerSelector = new DefaultHttpControllerSelector(config); HttpControllerDescriptor controllerDescriptor = controllerSelector.SelectController(request); return controllerDescriptor; }
Chcemy również weryfikować akcje, zatem potrzebujemy ActionDescriptor:
private static HttpActionDescriptor GetActionDescriptor(HttpConfiguration config, HttpControllerDescriptor controllerDescriptor, HttpRequestMessage request) { var routeData = (IHttpRouteData)request.Properties[HttpPropertyKeys.HttpRouteDataKey]; HttpControllerContext controllerContext = new HttpControllerContext(config, routeData, request); controllerContext.ControllerDescriptor = controllerDescriptor; IHttpActionSelector actionSelector = new ApiControllerActionSelector(); HttpActionDescriptor actionDescriptor = actionSelector.SelectAction(controllerContext); return actionDescriptor; }
Korzystając z powyższych metod pomocniczych, główna metoda Verify aktualnie wygląda następująco:
public static void Verify<T>(string url, Expression<Action<T>> mapping) { HttpConfiguration config = InitConfig(); HttpRequestMessage request = InitRequest(url, config); HttpControllerDescriptor controllerDescriptor = GetControllerDescriptor(config, request); HttpActionDescriptor actionDescriptor = GetActionDescriptor(config, controllerDescriptor, request); }
Asercja kontrolera jest bardzo prosta i wystarczy:
Assert.That(controllerDescriptor.ControllerType, Is.EqualTo(typeof(T)));
Największe wyzwanie to oczywiście akcja, wraz z parametrami. Z tego względu warto stworzyć kolejną metodę pomocniczą o następującej sygnaturze:
private static void AssertAction<T>(Expression<Action<T>> mapping, HttpActionDescriptor actionDescriptor,HttpRequestMessage request)
Asercja nazwy akcji również jest dość prosta:
var expectedMethod = (MethodCallExpression)mapping.Body; Assert.That(actionDescriptor.ActionName,Is.EqualTo(expectedMethod.Method.Name));
Najwięcej wysiłku musimy włożyć w wyodrębnienie argumentów z wyrażenia. Przede wszystkim potrzebujemy:
HttpParameterDescriptor[] actualParameters = actionDescriptor.GetParameters().ToArray(); var httpRouteData = (IHttpRouteData[]) routeData.Values.First().Value;
Pierwsza kolekcja to lista deskryptorów dla parametrów. Tak samo jak ControllerDescriptor oraz ActionDescriptor, opisują one po prostu typy i nazwy przekazanych argumentów. RouteData pochodzi z URL i zawiera faktyczne wartości, które zostały przekazane. Za pomocą RouteData oraz ActualParameters będziemy w stanie odczytać przekazane parametry.
Spójrzmy teraz na najważniejszą metodę, AssertParameters:
private static void AssertParameters(IHttpRouteData routeData, HttpParameterDescriptor[] actualParameters, MethodCallExpression expectedMethod) { for (int i = 0; i < expectedMethod.Arguments.Count; i++) { var expectedValue = ((ConstantExpression)expectedMethod.Arguments[i]).Value; var actualValue = routeData.Values[actualParameters[i].ParameterName]; Assert.That(actualValue, Is.EqualTo(expectedValue.ToString())); } }
Zmienna expectedValue pochodzi z przekazanego wyrażenia (lambda), a actualValue z wspomnianego właśnie RouteData. Kod jest bardzo uproszczony i nie przewiduje np. DateTime czy złożonych parametrów. Podsumowując, cały helper wygląda następująco:
public class RoutingAssert { public static void Verify<T>(string url, Expression<Action<T>> mapping) { HttpConfiguration config = InitConfig(); HttpRequestMessage request = InitRequest(url, config); HttpControllerDescriptor controllerDescriptor = GetControllerDescriptor(config, request); HttpActionDescriptor actionDescriptor = GetActionDescriptor(config, controllerDescriptor, request); Assert.That(controllerDescriptor.ControllerType, Is.EqualTo(typeof(T))); AssertAction(mapping,actionDescriptor,request); } private static void AssertAction(Expression<Action<T>> mapping, HttpActionDescriptor actionDescriptor,HttpRequestMessage request) { var routeData = GetRouteDataFromRequest(request); var expectedMethod = (MethodCallExpression)mapping.Body; Assert.That(actionDescriptor.ActionName,Is.EqualTo(expectedMethod.Method.Name)); HttpParameterDescriptor[] actualParameters = actionDescriptor.GetParameters().ToArray(); var httpRouteData = (IHttpRouteData[]) routeData.Values.First().Value; AssertParameters(httpRouteData[0], actualParameters, expectedMethod); } private static HttpActionDescriptor GetActionDescriptor(HttpConfiguration config, HttpControllerDescriptor controllerDescriptor, HttpRequestMessage request) { var routeData = GetRouteDataFromRequest(request); HttpControllerContext controllerContext = new HttpControllerContext(config, routeData, request); controllerContext.ControllerDescriptor = controllerDescriptor; IHttpActionSelector actionSelector = new ApiControllerActionSelector(); HttpActionDescriptor actionDescriptor = actionSelector.SelectAction(controllerContext); return actionDescriptor; } private static IHttpRouteData GetRouteDataFromRequest(HttpRequestMessage request) { return (IHttpRouteData)request.Properties[HttpPropertyKeys.HttpRouteDataKey]; } private static HttpControllerDescriptor GetControllerDescriptor(HttpConfiguration config, HttpRequestMessage request) { IHttpControllerSelector controllerSelector = new DefaultHttpControllerSelector(config); HttpControllerDescriptor controllerDescriptor = controllerSelector.SelectController(request); return controllerDescriptor; } private static HttpRequestMessage InitRequest(string url, HttpConfiguration config) { HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, url); var routeData = config.Routes.GetRouteData(request); request.Properties[HttpPropertyKeys.HttpRouteDataKey] = routeData; return request; } private static HttpConfiguration InitConfig() { var config = new HttpConfiguration(); config.MapHttpAttributeRoutes(); config.EnsureInitialized(); return config; } private static void AssertParameters(IHttpRouteData routeData, HttpParameterDescriptor[] actualParameters, MethodCallExpression expectedMethod) { for (int i = 0; i < expectedMethod.Arguments.Count; i++) { var expectedValue = ((ConstantExpression)expectedMethod.Arguments[i]).Value; var actualValue = routeData.Values[actualParameters[i].ParameterName]; Assert.That(actualValue, Is.EqualTo(expectedValue.ToString())); } } }
Jeśli metoda przyjmuje jako parametr wyrażenia lambda lub DateTime powyższy kod nie zadziała. Podobnie jest on ograniczony wyłącznie do HTTP GET. Myślę jednak, że taka próbka daje wystarczająco dużo informacji, aby rozszerzyć helper już we własnym zakresie.
Dzięki za wpis!
CodeHighlighter trochę szwankuje i niektóry kod nie jest widoczny w całości a scroll się nie pojawia 🙁
Zmieniłem rozmiar – teraz powinno byc juz OK.