Unit Test ASP.NET Core Middleware

26-02-2017
source code

Introduction

In this blog post I will explain how you can unit test aspnet core middleware. I will be using a middleware which was created in a different blog post that you can find here.

Middleware unit testing is tricky

The way middleware works is that it is hooked into a pipeline and receives an HttpContext then pass the work to the next middleware and so on. Unit testing a single middleware sometimes becomes challenging. For example if the middleware is expecting that certain values must exist in the context like certain header values or user credentials. Luckily we can overcome these challenges by using HttpContext base classes or mocked HttpContext depending on the situation as you will see in this blog post.

To demonstrate middleware unit testing, I will unit test 2 middlewares that I created in another blog post. These middlwares called LogRequestMiddleware and LogResponseMiddleware. For more information about these middlewares please check the blog post here.

LogRequestMiddleware unit test

This middleware is designed to log the HttpRequest values including query string and the target URL. As such this middleware needs to access the HttpRequest part of the HttpContext that it is receiving. Listing 1 shows the source for LogRequestMiddleware.

public class LogRequestMiddleware
{
    private readonly RequestDelegate next;
    private readonly ILogger _logger;
    private Func _defaultFormatter = (state, exception) => state;

    public LogRequestMiddleware(RequestDelegate next, ILogger logger)
    {
        this.next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        var requestBodyStream = new MemoryStream();
        var originalRequestBody = context.Request.Body;

        await context.Request.Body.CopyToAsync(requestBodyStream);
        requestBodyStream.Seek(0, SeekOrigin.Begin);

        var url = UriHelper.GetDisplayUrl(context.Request);
        var requestBodyText = new StreamReader(requestBodyStream).ReadToEnd();
        _logger.Log(LogLevel.Information, 1, $"REQUEST BODY: {requestBodyText}, REQUEST URL: {url}", null, _defaultFormatter);

        requestBodyStream.Seek(0, SeekOrigin.Begin);
        context.Request.Body = requestBodyStream;

        await next(context);
        context.Request.Body = originalRequestBody;
    }
}

For full explanation about this middleware please refere to this blog post. The meat of this middleware is in the Invoke method. In order to unit test this middleware we need to supply an HttpContext then we can assert on the ILogger to make sure that the HttpRequest was logged. Listing 2 shows source code for LogRequestMiddleware unit test.

[Fact]
public async void It_Should_Log_Request()
{
    var loggerMock = new Mock>();
    var requestMock = new Mock();        
    requestMock.Setup(x => x.Scheme).Returns("http");
    requestMock.Setup(x => x.Host).Returns(new HostString("localhost"));
    requestMock.Setup(x => x.Path).Returns(new PathString("/test"));
    requestMock.Setup(x => x.PathBase).Returns(new PathString("/"));
    requestMock.Setup(x => x.Method).Returns("GET");
    requestMock.Setup(x => x.Body).Returns(new MemoryStream());
    requestMock.Setup(x => x.QueryString).Returns(new QueryString("?param1=2"));


    var contextMock = new Mock();
    contextMock.Setup(x => x.Request).Returns(requestMock.Object);

    var logRequestMiddleware = new LogRequestMiddleware(next: (innerHttpContext) => Task.FromResult(0), logger: loggerMock.Object);


    await logRequestMiddleware.Invoke(contextMock.Object);
    loggerMock.Verify(m => m.Log(
            LogLevel.Information,
            It.IsAny(),
            It.Is(v => v.Contains("REQUEST BODY: , REQUEST URL: http://localhost//test?param1=2")),
            null,
            It.IsAny>()));
}

First I am arranging the objects that I will use when initialising or invoking the middleware. I am using moqzzz to create HTTPContext instance. I am also creating a mocked object for ILogger so I can assert on it later. The LogRequestMiddleware constructor takes a middleware and an instance of ILogger. The Invoke method on the other hand will take the mocked HTTPContext that we created.

I am asserting on the ILogger.Log function. The only bit I am interested in is the text that is being logged. That is why I am using It.IsAny on the other parameters but It.Is on the 3rd parameter which should include the logged HTTPRequest attributes. In this case it is the request body and full URL.

LogResponseMiddleware unit test

This middleware is designed to log the HttpResponse, namely the body which is what is returned to the user. As such this middleware needs to access the HttpResponse part of the HttpContext that it is receiving. Listing 3 shows the source code for LogResponseMiddleware.

public class LogResponseMiddleware
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    private Func _defaultFormatter = (state, exception) => state;

    public LogResponseMiddleware(RequestDelegate next, ILogger logger)
    {
        _next = next;
        _logger = logger;
    }

    public async Task Invoke(HttpContext context)
    {
        var bodyStream = context.Response.Body;

        var responseBodyStream = new MemoryStream();
        context.Response.Body = responseBodyStream;

        await _next(context);

        responseBodyStream.Seek(0, SeekOrigin.Begin);
        var responseBody = new StreamReader(responseBodyStream).ReadToEnd();
        _logger.Log(LogLevel.Information, 1, $"RESPONSE LOG: {responseBody}", null, _defaultFormatter);
        responseBodyStream.Seek(0, SeekOrigin.Begin);
        await responseBodyStream.CopyToAsync(bodyStream);
    }
}

For full explanation about this middleware please refer to zzz. Again the only bit we are interested in testing is the Invoke method. Listing 4 shows the unit test for LogResponseMiddleware

[Fact]
public async void It_Should_Log_Response()
{
    var loggerMock = new Mock>();            

    var logResponseMiddleware = new LogResponseMiddleware(next: async (innerHttpContext) => 
    {
        await innerHttpContext.Response.WriteAsync("test response body");
    }, logger: loggerMock.Object);

    await logResponseMiddleware.Invoke(new DefaultHttpContext());
    loggerMock.Verify(m => m.Log(
            LogLevel.Information,
            It.IsAny(),
            It.Is(v => v.Contains("RESPONSE LOG: test response body")),
            null,
            It.IsAny>()));
}

Testing this middleware is different but why? The LogResponseMiddleware executes when the response is returned to the user, i.e. It waits for all other middlewares to complete so it can read the full response body. This is why when initialising this middleware in testing I am supplying a middleware that writes some text into the HttpResponse. Here I am asserting against the ILogger.Log method to make sure the response text was logged.

Note that I am sending an instance of DefaultHttpContext to the Invoke method. I can't use a mocked HttpContext in this scenario because the first middleware (the lambda function that we passed to the constructor) will need to write to the response. Hence the HttpResponse needs to be a real object not mocked.

The DefaultHttpContext is a very handy class when it comes to unit testing because you can pass various elements to the constructor when initialising it like user of type ClaimsPrincipal or session of type ISession. Middleware usually works with such objects i.e. user and session to initialise authentication or store a user id so it is available for all middlewares from the session object.

Conclusion

In this blog post I have explained how to unit test a middleware using various techniques depending on how the middleware suppose to work. I have used mocked object and real object to achieve my goal depending on the scenario.