Как ExpressionTrees помогают тестировать WebApi

от автора

Всем хороши ApiController’ы, да не создают они WSDL и нельзя просто так взять и получить proxy. Да, ApiController’ы неплохо тестируются unit-test’ами. Но юниты пропускают ошибки транспортного уровня и в целом без парочки end-to-end сценариев как-то неудобно. Можно конечно смириться, взять HttpClient и написать примерно такой код:

HttpClient client = new HttpClient(); client.BaseAddress = new Uri("http://localhost:56851/");  // Add an Accept header for JSON format. client.DefaultRequestHeaders.Accept.Add(     new MediaTypeWithQualityHeaderValue("application/json"));  HttpResponseMessage response = client.GetAsync("api/User").Result;  if (response.IsSuccessStatusCode) {     var users = response.Content.ReadAsAsync&     lt;IEnumerable<Users>>().Result;     usergrid.ItemsSource = users;  } else {     MessageBox.Show("Error Code" +      response.StatusCode + " : Message - " + response.ReasonPhrase); } 

Но как же это муторно каждый раз лезть в описание контроллеров, проверять типы, короче хочется вот так:

var resp = GetResponse<SomeController>(c => gc.SomeAction(new Dto{val = "123"})); 

Как выяснилось, это вполне можно реализовать применив немного уличной магии деревья выражений

Получение информации об API

для начала нам нужно знать какое API вообще есть, для этого замапим роуты

[SetUp] public void SetUp() {     _cfg = new HttpConfiguration();     _cfg.Routes.MapHttpRoute(             name: "DefaultApi",         routeTemplate: "api/{controller}/{action}/{id}",         defaults: new { id = RouteParameter.Optional }     ); } 

Вызов удаленного метода

ApiDescriptions теперь знает о том, где искать контроллеры и любезно предоставит метаинформацию. В WebApi может быть много вариантов вызова одного метода: я никогда не использую два Http-метода для одного метода API, поэтому этот кейс меня не волнует. С чистой совестью возьмем первый подходящий метод

protected HttpResponseMessage GetResponse<T>(Expression<Action<T>> expression)     where T : ApiController {     var baseAddress = System.Configuration.ConfigurationManager.AppSettings["BaseAddress"];     var convert = (MethodCallExpression)expression.Body;     var name = convert.Method.Name;     var paramsName = convert.Method.GetParameters().Select(p => p.Name).ToArray();      var desc = _cfg.Services.GetApiExplorer().ApiDescriptions.First(         d =>             d.ActionDescriptor.ControllerDescriptor.ControllerType == typeof(T) &&             d.ActionDescriptor.ActionName == name);   //... 

Допущение №2. Кроме JSON мне ничего не интересно. Для get-методов и post-с примитивами в параметрах заменим вхождения вида paramName={paramName} на paramName=значение из Expression, которое мы передали.

using (var client = new HttpClient { BaseAddress = new Uri(baseAddress) }) {     client.DefaultRequestHeaders.Accept.Add(         new MediaTypeWithQualityHeaderValue("application/json"));      var relPath = desc.RelativePath;      var index = 0;      if (relPath.Contains("?"))     {         foreach (var pars in paramsName)         {             relPath = relPath.Replace(                 string.Format("{{{0}}}", pars.Name),                 InvokeExpression(convert.Arguments[index++], pars.ParameterType).Return(o => o.ToString(), string.Empty));         }      } 

InvokeExpression

Самый простой способ получить значение любого Expression — скомпилировать его в лямбду, что я и сделал. Откровенно говоря, мы знаем возвращаемый тип на этапе компиляции из типа контроллера. Но в этом случае придется делать отдельный кейс для методов, возвращающих void. В этом случае придется использовать Action вместо Func<T,TResult>. Такой код уже достаточно сложен для понимания. За производительностью я не гонюсь, — сетевые издержки сожрут все те наносекунды, которые будут сэкономлены на компиляции.

private static object InvokeExpression(Expression e, Type returnType) {     return Expression.Lambda(         typeof (Func<>).MakeGenericType(returnType),         e).Compile().DynamicInvoke(); } 

Получение результата

Осталось самое простое — получить результат и вызвать метод. Для Post методов считаем, что всегда есть родительский объект-wrapper. Возвращаем результат или падаем с ошибкой.

var uri = new Uri(new Uri(baseAddress), relPath);  var resp = desc.HttpMethod.Method == HttpMethod.Post.ToString()     ? client.PostAsJsonAsync(uri.ToString(), InvokeExpression(convert.Arguments.Single(),     desc.ParameterDescriptions.Single().ParameterDescriptor.ParameterType)).Result     : client.GetAsync(uri).Result;  if (resp.StatusCode == HttpStatusCode.InternalServerError) {     using (var sr = new StreamReader(resp.Content.ReadAsStreamAsync().Result))     {         throw new InvalidOperationException(sr.ReadToEnd());     }                     } return resp; 

В итоге

Получился вот такой метод

protected HttpResponseMessage GetResponse<T>(Expression<Action<T>> expression)     where T : ApiController {     var baseAddress = System.Configuration.ConfigurationManager.AppSettings["BaseAddress"];     var convert = (MethodCallExpression)expression.Body;     var name = convert.Method.Name;     var paramsName = convert.Method.GetParameters().ToArray();      var desc = _cfg.Services.GetApiExplorer().ApiDescriptions.First(         d =>             d.ActionDescriptor.ControllerDescriptor.ControllerType == typeof(T) &&             d.ActionDescriptor.ActionName == name);      using (var client = new HttpClient { BaseAddress = new Uri(baseAddress) })     {         client.DefaultRequestHeaders.Accept.Add(             new MediaTypeWithQualityHeaderValue("application/json"));          var relPath = desc.RelativePath;          var index = 0;          if (relPath.Contains("?"))         foreach (var pars in paramsName)         {             relPath = relPath.Replace(                 string.Format("{{{0}}}", pars.Name),                 InvokeExpression(convert.Arguments[index++], pars.ParameterType).Return(o => o.ToString(), string.Empty));         }                          var uri = new Uri(new Uri(baseAddress), relPath);          var resp = desc.HttpMethod.Method == HttpMethod.Post.ToString()             ? client.PostAsJsonAsync(uri.ToString(), InvokeExpression(convert.Arguments.Single(),             desc.ParameterDescriptions.Single().ParameterDescriptor.ParameterType)).Result             : client.GetAsync(uri).Result;          if (resp.StatusCode == HttpStatusCode.InternalServerError)         {             using (var sr = new StreamReader(resp.Content.ReadAsStreamAsync().Result))             {                 throw new InvalidOperationException(sr.ReadToEnd());             }                              }         return resp;     } } 

Много чего в этом коде не идеально, но свою цель он выполнят, теперь я могу писать такие тесты:

[Test] public void UserController_TokenValid_WrongTokenReturnFalse() {     var resp = GetResponse<UserController>(gc => gc.TokenValid("123"));     Assert.AreEqual(false, resp.Content.ReadAsAsync<bool>().Result); } 

Или более сложные, например такие:

var obj = new RoundResultDtoIn() {     LevelId = 3,     RoomName = "123",     RoundTime = 50,     StartDateTime = DateTime.Now };              GetResponse<GameController>(gc => gc.SaveResults(obj)); 

ссылка на оригинал статьи http://habrahabr.ru/post/202044/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *