How to Unit Test Custom Middleware in ASP.NET Core

Introduction

Middleware components in ASP.NET Core encapsulate a specific functionality and can modify or short-circuit requests based on the requirement.

We can create a custom Middleware in multiple ways – we can create an inline middleware that runs a few lines of code, or we can create a custom Class that encapsulates complex business logic and use dependencies.

You can check out my detailed article on How to create a custom Middleware in ASP.NET Core to learn more about the topic.

In this detailed article, let us learn how to write a unit test for a custom Middleware component in ASP.NET Core with an illustrating example.

Why should I unit test a custom Middleware code?

Unit Testing a code block verifies that all the possible scenarios for that code block is tested and covered for any expected behaviors. This ensures that the piece of code is reliable and does what is expected.

It is a good practice for developers to cover as much behavior in unit tests as possible, so that we can catch bugs in our code, early in the development process.

Since a custom Middleware component involves business logic, it is important that we cover the functionality with sufficient unit tests.

While writing unit tests for a custom Middleware component, we will test the behavior of the Middleware as an isolated component block rather than an actual Middleware.

We will verify if the functionality that the Middleware performs on the HTTP Request works as expected, without actually passing the Request itself.

Unit Testing ASP.NET Core Middleware with an Example

A typical Middleware component has two dependencies – RequestDelegate and HttpContext. We can mock the RequestDelegate and HttpContext and create tests to assert the business logic.

For example, let us consider the middleware component designed above – AddRequestHeaderValueToContextItems class that adds a request header value to context items.

namespace middlewareapi;

public static class KeyConstants
{
    public const string CUSTOM_REQUEST_HEADER_KEY = "X-My-Request-ID";
    public const string CUSTOM_REQUEST_CONTEXT_ITEMS_KEY = "xMyRequestId";
}

public class AddRequestHeaderValueToContextItems
{
    private readonly RequestDelegate _nextRequestDelegate;

    public AddRequestHeaderValueToContextItems(RequestDelegate nextRequestDelegate)
    {
        _nextRequestDelegate = nextRequestDelegate;
    }

    public async Task InvokeAsync(HttpContext httpContext)
    {
        Console.WriteLine("AddRequestHeaderValueToContextItems is now Invoked");

        var requestHeaders = httpContext.Request.Headers.ToDictionary(
            x => x.Key,
            x => x.Value.ToString()
        );

        if (requestHeaders.ContainsKey(KeyConstants.CUSTOM_REQUEST_HEADER_KEY))
        {
            var myCustomRequestHeaderValue = requestHeaders[KeyConstants.CUSTOM_REQUEST_HEADER_KEY];
            httpContext.Items.Add(KeyConstants.CUSTOM_REQUEST_CONTEXT_ITEMS_KEY, myCustomRequestHeaderValue);
        }

        await _nextRequestDelegate.Invoke(httpContext);
    }
}

We can unit test this functionality and ensure that it covers all the necessary use cases.

Designing a Test Class with Test case Methods

Our Test Class for the Middleware class is as below. It covers two Test Scenarios of the If condition present in the Middleware class.

I am using xUnit framework for unit testing and Moq for Mocking the dependencies.

public class AddRequestHeaderValueToContextItemsTests
{
    [Fact]
    public async Task CheckIfRequestIdContextItemIsSet_WhenRequestHeadersContainCustomRequestHeaderKey()
    {
        // Positive Scenario -
        // If the incoming Request Headers contain the configured CustomRequestHeaderKey
        // then the middleware must put the value of it inside the Context.Items
        // the Context.Items must contain a value for the key
    }

    [Fact]
    public async Task CheckIfRequestIdContextItemIsNotSet_WhenRequestHeadersDoesNotContainCustomRequestHeaderKey()
    {
        // Negative Scenario -
        // If the incoming Request Headers doesn't contain the configured CustomRequestHeaderKey
        // then the middleware must not put the value of it inside the Context.Items
        // the Context.Items must not contain a value for the key
    }
}

To write the test logic, we must first mock the two dependencies – RequestDelegate that is injected in the constructor and HttpContext that is injected in the method.

Once we prepare these two mock objects, we will simply create an instance of the AddRequestHeaderValueToContextItems class and call the InvokeAsync method.

Once the execution is complete, we will verify if the Context.Items dictionary inside the mock HttpContext object has an entry.

Mocking the HttpContext

We will set up a mock HttpContext object, where we will configure only the Request Headers Dictionary and Context Items for our use case. We will need to return a Dictionary when the middleware calls Request.Headers and an empty Dictionary when Context.Items is called.

I will write two mock methods – one for the positive case where the Request Headers contain a custom key and another method that does not contain any Request Headers.

public class AddRequestHeaderValueToContextItemsTests
{
    .
    .
    .
    .

    /* #region private methods */

    private HttpContext GetMockHttpContextWithCustomRequestHeader()
    {
        // mock value is seeded for HTTP REQUEST HEADER
        var mockRequestHeaders = new Dictionary<string, StringValues>()
        {
            { KeyConstants.CUSTOM_REQUEST_HEADER_KEY, "AliceBobert_927354" }
        };
        var moqHttpContext = new Mock<HttpContext>();
        moqHttpContext
            .Setup(x => x.Request.Headers)
            .Returns(new HeaderDictionary(mockRequestHeaders));

        // Mock Context.Items is setup that returns a Dictionary
        moqHttpContext.Setup(x => x.Items).Returns(new Dictionary<object, object?>());

        var mockHttpContext = moqHttpContext.Object;
        return mockHttpContext;
    }

    private HttpContext GetMockHttpContextWithoutCustomRequestHeader()
    {
        // no values are seeded for the HTTP REQUEST HEADERS
        var mockRequestHeaders = new Dictionary<string, StringValues>();
        var moqHttpContext = new Mock<HttpContext>();
        moqHttpContext
            .Setup(x => x.Request.Headers)
            .Returns(new HeaderDictionary(mockRequestHeaders));

        // Mock Context.Items is setup that returns a Dictionary
        moqHttpContext.Setup(x => x.Items).Returns(new Dictionary<object, object?>());

        var mockHttpContext = moqHttpContext.Object;
        return mockHttpContext;
    }

    /* #endregion */
}

Faking the RequestDelegate

We will use the RequestDelegate object to invoke the next delegate in the pipeline.

Since in a unit test we will not invoke any further delegates in the pipeline, we will simply create a fake RequestDelegate that returns an empty task.

The method looks like below –

private RequestDelegate GetFakeRequestDelegate()
{
    // since we don't need to call the next Delegate
    // we will create a request delegate that returns an empty task
    var fakeRequestDelegate = new RequestDelegate((innerContext) => Task.FromResult(0));
    return fakeRequestDelegate;
}

We will now write our Test methods that verify two test cases – in the positive case we will assert if the Context.Items Dictionary has a key for the CUSTOM_REQUEST_CONTEXT_ITEMS_KEY, and in the negative scenario we will assert if the dictionary doesn’t contain such key.

The complete Test class with all the methods is shown as below –

using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Primitives;
using Moq;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;

namespace middlewareapi.tests;

public class AddRequestHeaderValueToContextItemsTests
{
    public AddRequestHeaderValueToContextItemsTests() { }

    [Fact]
    public async Task CheckIfRequestIdContextItemIsSet_WhenRequestHeadersContainCustomRequestHeaderKey()
    {
        // Positive Scenario -
        // If the incoming Request Headers contain the configured CustomRequestHeaderKey
        // then the middleware must put the value of it inside the Context.Items
        // the Context.Items must contain a value for the key

        // Arrange
        var httpContext = GetMockHttpContextWithCustomRequestHeader();
        var requestDelegate = GetFakeRequestDelegate();

        // Act
        var addRequestHeaderValueToContextItems = new AddRequestHeaderValueToContextItems(
            requestDelegate
        );
        await addRequestHeaderValueToContextItems.InvokeAsync(httpContext);

        // Assert
        var value = Assert.Contains(
            KeyConstants.CUSTOM_REQUEST_CONTEXT_ITEMS_KEY,
            httpContext.Items
        );
        Assert.Equal("AliceBobert_927354", value);
    }

    private RequestDelegate GetFakeRequestDelegate()
    {
        // since we don't need to call the next Delegate
        // we will create a request delegate that returns an empty task
        var fakeRequestDelegate = new RequestDelegate((innerContext) => Task.FromResult(0));
        return fakeRequestDelegate;
    }

    private HttpContext GetMockHttpContextWithCustomRequestHeader()
    {
        // mock value is seeded for HTTP REQUEST HEADER
        var mockRequestHeaders = new Dictionary<string, StringValues>()
        {
            { KeyConstants.CUSTOM_REQUEST_HEADER_KEY, "AliceBobert_927354" }
        };
        var moqHttpContext = new Mock<HttpContext>();
        moqHttpContext
            .Setup(x => x.Request.Headers)
            .Returns(new HeaderDictionary(mockRequestHeaders));

        // Mock Context.Items is setup that returns a Dictionary
        moqHttpContext.Setup(x => x.Items).Returns(new Dictionary<object, object?>());

        var mockHttpContext = moqHttpContext.Object;
        return mockHttpContext;
    }

    [Fact]
    public async Task CheckIfRequestIdContextItemIsNotSet_WhenRequestHeadersDoesNotContainCustomRequestHeaderKey()
    {
        // Negative Scenario -
        // If the incoming Request Headers doesn't contain the configured CustomRequestHeaderKey
        // then the middleware must not put the value of it inside the Context.Items
        // the Context.Items must not contain a value for the key

        // Arrange
        var httpContext = GetMockHttpContextWithoutCustomRequestHeader();
        var requestDelegate = GetFakeRequestDelegate();

        // Act
        var addRequestHeaderValueToContextItems = new AddRequestHeaderValueToContextItems(
            requestDelegate
        );
        await addRequestHeaderValueToContextItems.InvokeAsync(httpContext);

        // Assert
        Assert.False(httpContext.Items.ContainsKey(KeyConstants.CUSTOM_REQUEST_CONTEXT_ITEMS_KEY));
    }

    private HttpContext GetMockHttpContextWithoutCustomRequestHeader()
    {
        // no values are seeded for the HTTP REQUEST HEADERS
        var mockRequestHeaders = new Dictionary<string, StringValues>();
        var moqHttpContext = new Mock<HttpContext>();
        moqHttpContext
            .Setup(x => x.Request.Headers)
            .Returns(new HeaderDictionary(mockRequestHeaders));

        // Mock Context.Items is setup that returns a Dictionary
        moqHttpContext.Setup(x => x.Items).Returns(new Dictionary<object, object?>());

        var mockHttpContext = moqHttpContext.Object;
        return mockHttpContext;
    }
}

Buy Me A Coffee
Enjoyed my content and found it useful?
Consider buying me a coffee to support my work and keep the content flowing!

Conclusion

Unit Testing in general is crucial for ensuring its reliability and functionality. By writing comprehensive tests, we can catch bugs early, validate expected behavior, and improve code quality.

Since custom Middleware components are used for specific functionalities, it is important that we cover these components in our Unit Tests.

In this article, we discussed how we can unit test a simple custom middleware component. We can use any Mocking framework such as Moq to mock the HttpContext according to our requirements and pass a Fake RequestDelegate while calling the InvokeAsync method on the class object.

Checkout my new ebook Exploring ASP.NET Core Middlewares – A Complete Guide for Developers – I will explain everything you need to know about working with Middleware components in ASP.NET Core!!😁

Sriram Mannava
Sriram Mannava

A software Engineer from Hyderabad, India with close to 9 years experience as a full stack developer in Angular and .NET space.

Articles: 5