Card image cap

Unit Testing Middleware Components in ASP.NET Core

ASP.NET Core xUnit  • Posted 3 months ago

Unit Testing refers to testing the functionality of a component in isolation. A Middleware is an individual component that is responsible for a single functionality in a request pipeline. In this article, let's talk about how we can "Unit Test" a "Middleware" component in ASP.NET Core.

Middlewares are one of the most powerful components in the ASP.NET Core framework, using which we can perform a particular functionality over an incoming request or an outgoing response. We can use them for practical purposes such as Logging, Authorization, Routing and for more complex scenarios that suit our requirements. Why we're talking all this now? Just to understand that a custom Middleware class can hold some business logic which does require to testing for any errors. And how can we do this better? by writing Unit Tests for the logic that happens in a Middleware just to ensure that everything happens as expected when an actual request passes by.

To illustrate this, let's take the example of a simple Middleware component inside our ReadersAPI which looks into the incoming Request Headers for an "X-Request-ID" header that uniquely identifies a request, and places it inside HttpContext.Items dictionary. If the incoming Request Headers don't contain this header, the Middleware generates a Guid to indicate a RequestId and places the same inside the Items dictionary.

For starters, HttpContext.Items is a dictionary which we can use to temporarily store data that persists across a single Request. It is a convenient datastructure we can use to pass along persistent data across components within a single Request scope. We set the "X-Request-ID" header value inside our HttpContext.Items and use it to aggregate all the Logs recorded for this particular Request in our Logging framework, which is something out of the scope of our goal.

Since this exercise occurs at a Request level, we can use a Middleware which gets this job done for every incoming request, before the request reaches its intended handler.

The component looks like below:

namespace ReadersApi.WebApi.Middlewares
{
    public class SetRequestContextItems
    {
        private readonly RequestDelegate _next;
        public const string XRequestIdKey = "X-Request-ID";

        public SetRequestContextItems(RequestDelegate next)
        {
            _next = next;
        }

        public async Task InvokeAsync(HttpContext context)
        {
            // Read the Incoming Request Headers for an xRequestId Header
            // If present in the Header pull it from the Headers
            // Else Create a new Guid that represents the xRequestId

            //Set the xRequestId value thus obtained into HttpContext.Items Dictionary

            var headers = context.Request.Headers
                    .ToDictionary(x => x.Key, x => x.Value.ToString());

            string xRequestIdValue = string.Empty;

            if (headers.ContainsKey(XRequestIdKey))
            {
                xRequestIdValue = headers[XRequestIdKey];
            }
            else
            {
                xRequestIdValue = Guid.NewGuid().ToString();
            }

            context.Items.Add(XRequestIdKey, xRequestIdValue);

            await _next.Invoke(context);
        }
    }
}

We can then register this component into our Request pipeline inside the Startup class as:

namespace ReadersApi.WebApi
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            ----

            app.UseMiddleware<SetRequestContextItems>();

            ----
        }
    }
}

To test this, let's just add a Test Endpoint that returns all the contents of HttpContext.Items dictionary for every request.

namespace ReadersApi.WebApi.Controllers
{
    [ApiController]
    [Route("[controller]")]
    public class ContextItemsController : ControllerBase
    {
        [HttpGet]
        public Dictionary<string, string> Get()
        {
            return this.HttpContext
                .Items
                .ToDictionary(x => x.Key.ToString(), x => x.Value.ToString());
        }
    }
}

Which returns this when invoked:

{
    "X-Request-ID":"44368617-07de-4819-ac8d-e07161b7a394"
}

Everything looks great till this point. Now let's add a Test class that simulates this functionality and checks if the Middleware works fine for the two possible scenarios of this functionality:

  • How it behaves when we pass an xRequestId header in the Request
  • How it behaves when we don't pass an xRequestId header

Notice that we're using xUnit to write our Test scripts on the SetRequestContextItems Middleware class for varying input cases. We write these scripts inside ReadersApi.Tests project which takes a project reference of the ReadersApi.WebApi project and is an xUnit Test project.

namespace ReadersApi.Tests
{
    public class SetRequestContextItemsTests
    {
        [Fact]
        public async Task 
          CheckIfRequestIdContextItemIsSet_WhenRequestHeadersContainXRequestId()
        {
            // Test Logic for the scenario happens here
        }

        [Fact]
        public async Task 
          CheckIfRequestIdContextItemIsSet_WhenRequestHeadersDontContainXRequestId()
        {
            // Test Logic for the scenario happens here
        }
    }
}

Mocking the Middleware dependencies for UnitTest:

Writing UnitTests is simple: we follow the 3A philosophy - Arrange, Act, Assert where we create an instance of the unit we're supposed to test with unreal dependencies if it needs any; and test how its own logic works in ideal conditions irrespective of its dependencies.

For any Middleware, we have two dependencies which we need to take care of:

  • HttpContext - represents the incoming Request Context
  • RequestDelegate - represents the next Action in the pipeline which needs to be invoked once the current Middleware completes execution

We need to pass the RequestDelegate instance to our Middleware constructor, while the HttpContext needs to be supplied as a parameter for the InvokeAsync() method.

Since we're doing a UnitTest on the Middleware class as any other concrete class and hence a real HttpContext can't be brought in, same with the RequestDelegate. Hence we shall supply Mock dependencies to our Middleware class to get things done.

We shall use Moq framework to create Mock dependencies for us to use in our Unit Tests.

Mocking the HttpContext:

Since HttpContext is an abstract class, we can't directly create an instance of it. But to supply an instance of HttpContext for our purpose, we can create a Mock HttpContext class and pass it off.

Mock<HttpContext> httpContextMoq = new Mock<HttpContext>();
// some setup
HttpContext context = httpContextMoq.Object;

But the real question here is: do we need to mock everything inside a HttpContext for our purpose? The answer is: no. Its not really required to sit and mock each and every attribute of HttpContext object in order to create a real Mock HttpContext for our purpose. Let's just look at what parts of HttpContext are we using in our business logic in the Middleware class. We use only for two reasons:

  • To read data from HttpContext.Request.Headers
  • To insert data into HttpContext.Items

So we can just create a Mock HttpContext and setup only the Request.Headers and Items dictionary so that the Test won't throw us Object Reference when it tries to access these properties from the mock HttpContext object.

Let's setup the HttpContext object for our first Test case - when XRequestIdKey is present in the Request Headers.

// Arrange

// Mocking the HttpContext.Request.Headers 
// when xRequestIdKey Header is present
var headers = new Dictionary<string, StringValues>() {
    { SetRequestContextItems.XRequestIdKey, "123456" }
};

var httpContextMoq = new Mock<HttpContext>();
httpContextMoq.Setup(x => x.Request.Headers)
    .Returns(new HeaderDictionary(headers));
httpContextMoq.Setup(x => x.Items)
    .Returns(new Dictionary<object, object>());

var httpContext = httpContextMoq.Object;

Observe that we're also setting up the Items dictionary so that when the Middleware tries to push some value into the dictionary it shouldn't be acting on a null property, which is the default for any property in the Mocked object that is not setup. So we pass an empty dictionary so that later on we can check whether this dictionary has any values added in it.

Faking the RequestDelegate:

As mentioned before, RequestDelegate represents the next Middleware which needs to be invoked in the Request pipeline. Since we don't need to invoke any other Task later on in our Test, we can just Fake it with an empty Action that just does nothing. We just create a new RequestDelegate object and pass in an action that simply returns a completed task with a non-error code.

// Arrange

var requestDelegate = new RequestDelegate(
        (innerContext) => Task.FromResult(0));

Finally, we can just create an instance of our Middleware class and pass these mock/fake dependencies we just created and run to see how this works.

The complete Test method looks like below:

[Fact]
public async Task 
    CheckIfRequestIdContextItemIsSet_WhenRequestHeadersContainXRequestId()
{
    // Arrange

    // Create an instance of SetRequestContextItems() class
    // Call the InvokeAsync() method
    // Check if the resultant HttpContext.Items Dictionary 
    // contains xRequestIdKey with some Value

    // SetRequestContextItems() class requires two dependencies
    // HttpContext and RequestDelegate
    // We need to mock HttpContext.Request and RequestDelegate for our case
    // The Business Logic runs on HttpContext.Request.Headers Dictionary and 
    // not any other HttpContext attribute, 
    // so we can safely mock only the Request.Headers part
    // RequestDelegate is an ActionDelegate that invokes the next Task
    // Hence we can just pass a dummy Action as the next Task to perform

    // Mocking the HttpContext.Request.Headers when xRequestIdKey Header is present
    var headers = new Dictionary<string, StringValues>() {
        { SetRequestContextItems.XRequestIdKey, "123456" }
    };

    var httpContextMoq = new Mock<HttpContext>();
    httpContextMoq.Setup(x => x.Request.Headers)
        .Returns(new HeaderDictionary(headers));
    httpContextMoq.Setup(x => x.Items)
        .Returns(new Dictionary<object, object>());

    var httpContext = httpContextMoq.Object;


    var requestDelegate = new RequestDelegate(
            (innerContext) => Task.FromResult(0));

    // Act
    var middleware = new SetRequestContextItems(
            requestDelegate);
    await middleware.InvokeAsync(httpContext);

    // Assert

    // check if the HttpContext.Items dictionary
    // contains any Key with XRequestIdKey
    // which implies that the Middleware
    // was able to place a value inside the Items
    // dictionary which is the expectation

    Assert.True(
        httpContext.Items.ContainsKey(SetRequestContextItems.XRequestIdKey));
}

Similarly, for the other TestCase where the Request headers don't contain the xRequestId header, we just pass an empty RequestHeaders instead of a Dictionary with a value inside it.

[Fact]
public async Task 
  CheckIfRequestIdContextItemIsSet_WhenRequestHeadersDontContainXRequestId()
{
    // Arrange

    // Create an instance of SetRequestContextItems() class
    // Call the InvokeAsync() method
    // Check if the resultant HttpContext.Items Dictionary 
    // contains xRequestIdKey with some Value

    // SetRequestContextItems() class requires two dependencies
    // HttpContext and RequestDelegate
    // We need to mock HttpContext.Request and RequestDelegate for our case
    // The Business Logic runs on HttpContext.Request.Headers Dictionary and 
    // not any other HttpContext attribute, 
    // so we can safely mock only the Request.Headers part
    // RequestDelegate is an ActionDelegate that invokes the next Task
    // Hence we can just pass a dummy Action as the next Task to perform

    // Mocking the HttpContext.Request.Headers when xRequestIdKey Header is not present
    var headers = new Dictionary<string, StringValues>();

    var httpContextMoq = new Mock<HttpContext>();
    httpContextMoq.Setup(x => x.Request.Headers)
        .Returns(new HeaderDictionary(headers));
    httpContextMoq.Setup(x => x.Items)
        .Returns(new Dictionary<object, object>());

    var httpContext = httpContextMoq.Object;


    var requestDelegate = new RequestDelegate(
            (innerContext) => Task.FromResult(0));

    // Act
    var middleware = new SetRequestContextItems(
            requestDelegate);
    await middleware.InvokeAsync(httpContext);

    // Assert

    // check if the HttpContext.Items dictionary
    // contains any Key with XRequestIdKey
    // which implies that the Middleware
    // was able to place a value inside the Items
    // dictionary which is the expectation

    Assert.True(
        httpContext.Items.ContainsKey(
            SetRequestContextItems.XRequestIdKey));
}

When we run these Tests either by VisualStudio or by dotnet CLI, we can see that our tests pass implying that the middleware works as expected.

> dotnet test

Test run for \ReadersApi.Tests\bin\Debug\netcoreapp3.1\ReadersApi.Tests.dll(.NETCoreApp,Version=v3.1)
Microsoft (R) Test Execution Command Line Tool Version 16.6.0
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...

A total of 1 test files matched the specified pattern.

Test Run Successful.
Total tests: 2
     Passed: 2
 Total time: 3.5400 Seconds

data/Admin/2020/8/unittest-output.png

In this way, we can Unit Test a middleware component inside our ASP.NET Core applications, by contextually mocking the HttpContext and faking the RequestDelegate.

Enjoying my posts?
You can now show me your support! 😊

What is the difference between Run() and Use() methods in IApplicationBuilder?

* Use() method: Used to create a simple middleware which can be "chained" to other functions over the pipeline. Takes two arguments: RequestDelegate ...


What is the difference between Response.Redirect() and Server.Transfer() ?

* Response.Redirect() redirects browser to another page, history is updated, trip back to client where browser loads the new page. * Server.Transfer( ...


How do you handle errors Globally in ASP.NET Core?

We can make use of the built-in UseExceptionHandler() middleware for catching Global Errors in ASP.NET Core. ``` app.UseExceptionHandler(err => ...


How do you design a strongly-typed class for a configuration?

To create a strongly-typed class for binding to a configuration section: * The property names and their types match the key names and their value t ...


How can you bind a configuration section to an object?

A Configuration section can be bound to a strictly-typed class object in two ways: * use Configuration.Bind() by passing the configuration section to ...