ASP.NET Web API integration testing with in-memory hosting

Β· 1864 words Β· 9 minutes to read

In-memory hosting is one of the hidden gems of ASP.NET Web API. While the community, forums, bloggers have been buzzing about web-host and self-host capabilities of Web API, aside from the terrific post by Pedro Felix, very little has been said about in memory hosting.

Let me show you an example today, how a lightweight Web API server can be temporarily established in memory (without elevated priviliges, or cannibalizing ports like self host) and used to perform integration testing, allowing you to test almost the entire pipeline, from the request to the response.

Intro to in memory hosting πŸ”—

In memory hosting comes down to creating an instance of HttpServer class. HttpServer itself is derived from HttpMessageHandler, and can be treated like one. It allows you to perform direct communication between the server runtime, and the client side, through the use of HttpClient or HttpMessageInvoker. As soon as the purpose for the existance of the server is fulfilled, you can very easily dispose of it.

As mentioned, the main advantage of this over self-host, is that it doesn’t require a port or a base address; since by design it doesn’t run on localhost, but truly in memory. You can pretty much mock the entire server environment/runtime in memory!

All this makes in-memory hosting incredibly useful and powerful tool for various testing scenarios, especially for integration testing. It allows you to quickly test the entire processing pipeline of your server, including controllers, filters/attributes, message handlers and formatters. From the moment the HTTP request leaves the client, all the way to the point when the response is received.

We will use integration testing as an example of application of in-memory hosting. A disclaimer here: it will by no means be fullly-fledged, production-ready testing sample or methodology walkthrough; rather my focus here is to show how you can leverage on Web API in-memory hosting to facilitate integration testing scenarios.

My test application πŸ”—

In my test application, I would like to test the following things:

  • submitting a request
  • a message handler
  • custom action filter attribute
  • controller action
  • JSON formatter applied to the incoming response
  • receiving the response

I will scrape the application together from the various pieces of code I have been blogging about over the weeks. The repository will come from this post, the filter will be a simplified version of the caching filter from here, and the handler will be the api key handler from here.

The handler will check for an “apikey”, if the request doesn’t have it, then error will be returned.

public class WebApiKeyHandler : DelegatingHandler  
{  
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)  
{  
string apikey = HttpUtility.ParseQueryString(request.RequestUri.Query).Get("apikey");

if (string.IsNullOrWhiteSpace(apikey)) {  
return SendError("You can't use the API without the key.", HttpStatusCode.Forbidden);  
} else {  
return base.SendAsync(request, cancellationToken);  
}  
}

private Task<HttpResponseMessage> SendError(string error, HttpStatusCode code)  
{  
var response = new HttpResponseMessage();  
response.Content = new StringContent(error);  
response.StatusCode = code;  
return Task<HttpResponseMessage>.Factory.StartNew(() => response);  
}  
}  

The filter’s role will be to add cache control directives to the response. That is what I will be checking for in the tests.

public class WebApiOutputCacheAttribute : ActionFilterAttribute  
{  
// client cache length in seconds  
private int _clientTimeSpan;

public WebApiOutputCacheAttribute(int clientTimeSpan)  
{  
_clientTimeSpan = clientTimeSpan;  
}

public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)  
{  
var cachecontrol = new CacheControlHeaderValue();  
cachecontrol.MaxAge = TimeSpan.FromSeconds(_clientTimeSpan);  
cachecontrol.MustRevalidate = true;  
actionExecutedContext.ActionContext.Response.Headers.CacheControl = cachecontrol;  
}  
}  

Setting up test project πŸ”—

My favorite testing framework is xUnit, but obviously any one would do here. Let’s add the test project then, download xUnit and get going.

I will set up the server in the test class’ constructor.

private HttpServer _server;  
private string _url = "/";  
public WebApiIntegrationTests()  
{  
var config = new HttpConfiguration();  
config.Routes.MapHttpRoute(name: "Default", routeTemplate: "api/{controller}/{action}/{id}", defaults: new { id = RouteParameter.Optional });  
config.IncludeErrorDetailPolicy = IncludeErrorDetailPolicy.Always;  
config.MessageHandlers.Add(new WebApiKeyHandler());  
_server = new HttpServer(config);  
}  

The configuration of the in-memory Web API host is almost exactly the same as configuring web- or self- host. You need specify, at the very least, the routes to be used, and that is done through the familiar HttpConfiguration. Server is instantiated by passing the configuration to the HttpServer constructor.

Notice how I wire up the Message Handler which I will be wanting to test in the server’s configuration. The server’s instance itself will be a private property of the class and will be reused between different test methods.

Since our test class will implement_IDisposable_, we need to cater for the Dispose method:

public void Dispose()  
{  
if (_server != null)  
{  
_server.Dispose();  
}  
}  

Now, what I will be testing against is:

  • get all items from the repository via GET
  • get single item from the repository via POST
  • remove single item from the repository via POST
  • get all items from the repository via GET without API key (fail on purpose)

In each case we will be starting off by constructing the HttpRequestMessage, sending it to our in memory server, and receiving its response in the form of HttpResponseMessage, which will be used for all kinds of Asserts. What’s important to re-iterate here, this is all simulating the normal end-to-end behavior of the Web API - so in other words a great integration test.

I will be using HttpClient’s SendAsync method for sending data to the server, since it accepts HttpRequestMessage, rather than POCO objects, so would allow us to simulate the behavior of the browser.

Before proceeding, I will add up two private helpers, which will be used for constructing HttpRequestMessages to be sent to the server - one for generic HttpRequestMessage and one with an ObjectContent.

private HttpRequestMessage createRequest(string url, string mthv, HttpMethod method)  
{  
var request = new HttpRequestMessage();

request.RequestUri = new Uri(_url + url);  
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(mthv));  
request.Method = method;

return request;  
}

private HttpRequestMessage createRequest<T>(string url, string mthv, HttpMethod method, T content, MediaTypeFormatter formatter) where T : class  
{  
HttpRequestMessage request = createRequest(url, mthv, method);  
request.Content = new ObjectContent<T>(content, formatter);

return request;  
}  

Get all items πŸ”—

[Fact]  
public void GetAllUrls()  
{  
var client = new HttpClient(_server);  
var request = createRequest("api/url/get?apikey=test", "application/json", HttpMethod.Get);  
var expectedJson = "[{"UrlId":1,"Address":"/2012/03/build-facebook-style-infinite-scroll-with-knockout-js-and-last-fm-api/","Title":"Build Facebook style infinite scroll with knockout.js and Last.fm API","Description":"Since knockout.js is one of the most amazing and innovative pieces of front-end code I have seen in recent years, I hope this is going to help you a bit in your everday battles. In conjuction with Last.FM API, we are going to create an infinitely scrollable history of your music records – just like the infinite scroll used on Facebook or on Twitter.","CreatedAt":"2012-03-20T00:00:00&#8243;,"CreatedBy":"Filip"},{"UrlId":2,"Address":"/2012/04/your-own-sports-news-site-with-espn-api-and-knockout-js/","Title":"Your own sports news site with ESPN API and Knockout.js","Description":"You will be able to browse the latest news from ESPN from all sports categories, as well as filter them by tags. The UI will be powered by KnockoutJS and Twitter bootstrap, and yes, will be a single page. We have already done two projects together using knockout.js – last.fm API infinite scroll and ASP.NET WebAPI file upload. Hopefully we will continue our knockout.js adventures in an exciting, and interesting for you, way.","CreatedAt":"2012-04-08T00:00:00&#8243;,"CreatedBy":"Filip"},{"UrlId":3,"Address":"/2012/04/rss-atom-mediatypeformatter-for-asp-net-webapi/","Title":"RSS & Atom MediaTypeFormatter for ASP.NET WebAPI","Description":"Today we are going to build a custom formatter for ASP.NET WebAPI, deriving from MediaTypeFormatter class. It will return our model (or collection of models) in RSS or Atom format.","CreatedAt":"2012-04-22T00:00:00&#8243;,"CreatedBy":"Filip"}]";

using (HttpResponseMessage response = client.SendAsync(request).Result)  
{  
Assert.NotNull(response.Content);  
Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);  
Assert.Equal(3, response.Content.ReadAsAsync<IQueryable<Url>>().Result.Count());  
Assert.Equal(60, response.Headers.CacheControl.MaxAge.Value.TotalSeconds);  
Assert.Equal(true, response.Headers.CacheControl.MustRevalidate);  
Assert.Equal(expectedJson, response.Content.ReadAsStringAsync().Result);  
}

request.Dispose();  
}  

So we create the HttpClient and pass our server to it, as if we were passing the DelegatingHandler - how cool is that? I have an expected JSON response, in the string form, which I will use to compare with the actual result. Then we have a bunch of other asserts:

  • response not null
  • response content type is “application/json”
  • object extracted from the response, IQueryable of Url, contains 3 items (to check if the apikey handler didn’t stop the request)
  • two CacheControl asserts (to check if our ActionFilter works)
  • comparison to the the expected Json (to check if JSON formatter works)

So, in a few lines of simple code, we have tested so much of the actual, real world functionalities.

Get single item πŸ”—

[Fact]  
public void PostSingleUrl()  
{  
var client = new HttpClient(_server);  
var newUrl = new Url() { Title = "Test post", Address = "http://www.strathweb.com", Description = "This is test post", CreatedAt = DateTime.Now, CreatedBy = "Filip", UrlId = 4 };  
var request = createRequest("api/url/post?apikey=test", "application/json", HttpMethod.Post, newUrl, new JsonMediaTypeFormatter());  
var expectedJson = JsonConvert.SerializeObject(newUrl);  
//"{"UrlId":4,"Address":"https://www.strathweb.com","Title":"Test post","Description":"This is test post","CreatedAt":"2012-06-10T23:23:22.8292873+02:00&#8243;,"CreatedBy":"Filip"}"

using (HttpResponseMessage response = client.SendAsync(request, new CancellationTokenSource().Token).Result)  
{  
Assert.NotNull(response.Content);  
Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);  
Assert.Equal(newUrl, response.Content.ReadAsAsync<Url>().Result);  
Assert.Equal(expectedJson, response.Content.ReadAsStringAsync().Result);  
}

request.Dispose();  
}  

This is very similar to the one before, except this time we will be submitting an object. I instantiate Url object, and send it to the controller. I can serialize the object using JSON.NET and compare the output (the controller returns the added object) - both by reading the content as string and comparing to JSO.NET serialized variable, and by reading the underlying type from the response.

Remove single object πŸ”—

[Fact]  
public void PostRemoveSingleUrl()  
{  
var client = new HttpClient(_server);  
var request = createRequest("api/url/remove/1?apikey=test", "application/json", HttpMethod.Post);  
var expectedJson = "{"UrlId":1,"Address":"/2012/03/build-facebook-style-infinite-scroll-with-knockout-js-and-last-fm-api/","Title":"Build Facebook style infinite scroll with knockout.js and Last.fm API","Description":"Since knockout.js is one of the most amazing and innovative pieces of front-end code I have seen in recent years, I hope this is going to help you a bit in your everday battles. In conjuction with Last.FM API, we are going to create an infinitely scrollable history of your music records – just like the infinite scroll used on Facebook or on Twitter.","CreatedAt":"2012-03-20T00:00:00&#8243;,"CreatedBy":"Filip"}"; 

using (HttpResponseMessage response = client.SendAsync(request, new CancellationTokenSource().Token).Result)  
{  
Assert.NotNull(response.Content);  
Assert.Equal("application/json", response.Content.Headers.ContentType.MediaType);  
Assert.Equal(1, response.Content.ReadAsAsync<Url>().Result.UrlId);  
Assert.Equal(expectedJson, response.Content.ReadAsStringAsync().Result);  
}  
request.Dispose();  
}  

In this test, I will be removing an item from the repository. This is done by posting an item ID as integer. The controller returns the deleted object instance. Again, I can compare the JSON output to the expected one, as well as extract the underlying POCO object and compare that (in this case I’m just comparing its ID).

Request without apikey πŸ”—

[Fact]  
public void GetAllUrlsNoApiKey()  
{  
var client = new HttpClient(_server);  
var request = createRequest("api/url/get", "application/json", HttpMethod.Get);

using (HttpResponseMessage response = client.SendAsync(request).Result)  
{  
Assert.NotNull(response.Content);  
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);  
Assert.Equal("You can't use the API without the key.", response.Content.ReadAsStringAsync().Result);  
}

request.Dispose();  
}  

This last test is an expected failure, as we make a request without providing the apikey. We can verify that the handler works just fine, and that the returned response is a denial of access (403 code).

Running the test πŸ”—

We can now compile the test project, and run the tests in xUnit. Since I am allergic to consoles, let’s use the GUI.

As you can see all the tests pass like a charm. This helps us to verify that it’s not just our unit logic that’s correct, but the entire pipeline behaves like it should. In a few simple methods, we have managed to test so much, by working only with HTTP request and HTTP response; all thanks to in memory hosting.

Summary and source code πŸ”—

As mentioned before, to me in memory hosting is one of the hidden gems of ASP.NET Web API. You should really take advantage of this awesome funcionality. It takes unit/integration testing to new heights, allowing you to produce even more robust applications.

Till next time!

About


Hi! I'm Filip W., a software architect from ZΓΌrich πŸ‡¨πŸ‡­. I like Toronto Maple Leafs πŸ‡¨πŸ‡¦, Rancid and quantum computing. Oh, and I love the Lowlands 🏴󠁧󠁒󠁳󠁣󠁴󠁿.

You can find me on Github, on Mastodon and on Bluesky.

My Introduction to Quantum Computing with Q# and QDK book
Microsoft MVP