Unit testing a custom middleware in ASP.NET Core API
Middlewares are a fundamental part of any ASP.NET Core application, introduced from day one. They are executed in the order they are added on every request, and could be considered similar to the HttpHandlers
and HttpModules
of the classic ASP.NET. Since middlewares can execute code before or after calling the next in the pipeline, they are considered ideal for a ton of various application features, including exception handling, logging, authentication etc.
You can learn more about middlewares by learning how to create a simple exception handling middleware.
Middlewares offer a powerful and flexible solution and for that, they need to be tested! Thankfully there is a way to do that so let’s first see what we are going to test and then just get in to it (middleware taken from Microsoft Docs):
using Microsoft.AspNetCore.Http;
using System.Globalization;
using System.Threading.Tasks;
namespace Culture
{
public class RequestCultureMiddleware
{
private readonly RequestDelegate _next;
public RequestCultureMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var cultureQuery = context.Request.Query["culture"];
if (!string.IsNullOrWhiteSpace(cultureQuery))
{
var culture = new CultureInfo(cultureQuery);
CultureInfo.CurrentCulture = culture;
CultureInfo.CurrentUICulture = culture;
}
// Call the next delegate/middleware in the pipeline
await _next(context);
}
}
}
What it actually does is setting up the current culture using the query string from HttpContext
. That is nice and useful, but HttpContext
is notoriously difficult and dangerous to mock because it is so easy to take a direct dependency on it. I ‘ve even read somewhere that mocking HttpContext
is like trying to calculate the last digit of pi: when you think you are done you ‘ll have more.
HttpContext encapsulates all HTTP-specific information about an individual HTTP request. It is the biggest object made by man.
To the rescue for this, we have DefaultHttpContext. Nothing to mock, saved from messing up with login information, it’s there waiting to be used. The only thing to keep in mind is the Response.Body
property. The Response.Body
stream in DefaultHttpContext
is Stream.Null
, which is a stream that just ignores all reads/writes. No exceptions, no nothing if you use it, just a “talk to the hand” attitude! Solution of course, to setup the Stream ourselves before calling the method:
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
Now that we found DefaultHttpContext
and know how to use it, let’s just write a unit test to check the default current culture:
[TestMethod]
public async Task RequestCultureMiddleware_DefaultBehaviour()
{
//Create a new instance of the middleware
var middleware = new RequestCultureMiddleware(
next: (innerHttpContext) =>
{
innerHttpContext.Response.WriteAsync(CultureInfo.CurrentCulture.TwoLetterISOLanguageName);
return Task.CompletedTask;
}
);
//Create the DefaultHttpContext
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
//Call the middleware
await middleware.InvokeAsync(context);
//Don't forget to rewind the stream
context.Response.Body.Seek(0, SeekOrigin.Begin);
var body = new StreamReader(context.Response.Body).ReadToEnd();
//'en' seems OK for me as the default
Assert.AreEqual("en", body);
}
Comments are relieving as to what is happening: We are passing a RequestDelegate
as the next argument of the RequestCultureMiddleware
constructor, that writes in the response the two letter ISO language name of the culture we set. In simple words, if the RequestCultureMiddleware
works correctly all next middlewares in the pipeline after it, should have their culture set to whatever the user sent as a query string.
And let’s test exactly that now:
[TestMethod]
public async Task RequestCultureMiddleware_SetToGerman()
{
//Create a new instance of the middleware
var middleware = new RequestCultureMiddleware(
next: (innerHttpContext) =>
{
innerHttpContext.Response.WriteAsync(CultureInfo.CurrentCulture.TwoLetterISOLanguageName);
return Task.CompletedTask;
}
);
//Create the DefaultHttpContext
var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream();
//Set culture to de-DE
context.Request.QueryString = context.Request.QueryString.Add("culture", "de-DE");
//Call the middleware
await middleware.InvokeAsync(context);
//Don't forget to rewind the stream
context.Response.Body.Seek(0, SeekOrigin.Begin);
var body = new StreamReader(context.Response.Body).ReadToEnd();
//This time, 'de' should be the two letter ISO language name
Assert.AreEqual("de", body);
}
And that was it! Easy right? Happy and safe middlewaring everybody!