📜 ⬆️ ⬇️

How ExpressionTrees Help Testing WebApi

ApiControllers are good for everyone, but they don’t create WSDL and you can’t just get and get a proxy. Yes, ApiControllers are well tested by unit-tests. But units skip transport-level errors and, in general, without a couple of end-to-end scenarios are somehow inconvenient. You can, of course, accept, take HttpClient and write something like this code:

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<IEnumerable<Users>>().Result; usergrid.ItemsSource = users; } else { MessageBox.Show("Error Code" + response.StatusCode + " : Message - " + response.ReasonPhrase); } 

But how is it a chore to climb into the description of the controllers every time, check the types, in short, I want to do this:
 var resp = GetResponse<SomeController>(c => gc.SomeAction(new Dto(){val = "123"})); 

As it turned out, it is quite possible to implement using a bit of street magic expression trees

Getting API information

for a start, we need to know what kind of API there is, for this we will zapim routes
 [SetUp] public void SetUp() { _cfg = new HttpConfiguration(); _cfg.Routes.MapHttpRoute( name: "DefaultApi", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional } ); } 

Calling a remote method

ApiDescriptions now knows where to look for controllers and will kindly provide meta information. In WebApi, there are many options for calling one method: I never use two Http methods for one API method, so this case does not bother me. With a clear conscience, take the first suitable method.
 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 pars = convert.Method.GetParameters().ToArray(); var desc = _cfg.Services.GetApiExplorer().ApiDescriptions.First( d => d.ActionDescriptor.ControllerDescriptor.ControllerType == typeof(T) && d.ActionDescriptor.ActionName == name); //... 


Assumption # 2. Besides JSON, I am not interested in anything. For getters and post-with primitives in the parameters, replace the entries of the form paramName = {paramName} with paramName = value from the Expression, which we passed.
 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 p in pars) { relPath = relPath.Replace( string.Format("{{{0}}}", pars.Name), InvokeExpression(convert.Arguments[index++], p.ParameterType).Return(o => o.ToString(), string.Empty)); } } 

InvokeExpression

The easiest way to get the value of any expression is to compile it into a lambda, which I did. Frankly, we know the return type at compile time from the controller type. But in this case you will have to do a separate case for methods that return void. In this case, you have to use Action instead of Func <T, TResult>. This code is already quite complicated to understand. I’m not chasing performance, network costs will eat all those nanoseconds that will be saved on compilation.
 private static object InvokeExpression(Expression e, Type returnType) { return Expression.Lambda( typeof (Func<>).MakeGenericType(returnType), e).Compile().DynamicInvoke(); } 

Getting the result

The simplest thing is to get the result and call the method. For Post methods we assume that there is always a parent wrapper object. We return the result or fall with an error.
 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; 

Eventually

Such method turned out here
 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 pars = 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 p in pars) { relPath = relPath.Replace( string.Format("{{{0}}}", p.Name), InvokeExpression(convert.Arguments[index++], p.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; } } 

A lot of things in this code are not perfect, but it will fulfill its goal, now I can write such tests:
 [Test] public void UserController_TokenValid_WrongTokenReturnFalse() { var resp = GetResponse<UserController>(gc => gc.TokenValid("123")); Assert.AreEqual(false, resp.Content.ReadAsAsync<bool>().Result); } 

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

')

Source: https://habr.com/ru/post/202044/


All Articles