George Kosmidis

Microsoft MVP | Speaks of Azure, AI & .NET | Founder of Munich .NET
Building tomorrow @
slalom
slalom

Unit testing a custom middleware in ASP.NET Core API

by George Kosmidis / Published 5 years ago, modified 4 years ago

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!

This page is open source. Noticed a typo? Or something unclear?
Edit Page Create Issue Discuss
Microsoft MVP - George Kosmidis
Azure Architecture Icons - SVGs, PNGs and draw.io libraries