ASP.NET Core – How to unit test your middleware class

There are three requirements for unit testing a middleware class:

  • Mock out RequestDelegate by defining a lambda that returns a Task (Task.FromException, Task.FromResult, or Task.FromCanceled).
  • Mock out HttpContext by using DefaultHttpContext.
  • The middleware function needs to be awaited, so your unit test needs to be defined with async Task.

Here’s an example:

[TestMethod()] public async Task ExceptionHandlingMiddlewareTest_Returns500StatusCode() { //arrange var expectedException = new ArgumentNullException(); RequestDelegate mockNextMiddleware = (HttpContext) => { return Task.FromException(expectedException); }; var httpContext = new DefaultHttpContext(); var exceptionHandlingMiddleware = new ExceptionHandlingMiddleware(mockNextMiddleware); //act await exceptionHandlingMiddleware.InvokeAsync(httpContext); //assert Assert.AreEqual(HttpStatusCode.InternalServerError, (HttpStatusCode)httpContext.Response.StatusCode); }
Code language: C# (cs)

This is a simple test that only checks the response status code. By passing in DefaultHttpContext, you have control over the request and response objects. You can set the request to whatever you need, and then verify the response. I’ll show examples of modifying the request and verifying the response below.

Verifying the contents of the response body

DefaultHttpContext.Response.Body is a stream, and it’s initialized to NullStream. If you want to be able to verify the contents of the response body, then you can initialize HttpContext.Response.Body to a MemoryStream, and use a StreamReader to get the contents.

Here’s an example:

[TestMethod()] public async Task ExceptionHandlingMiddlewareTest_WritesExceptionResponseJsonToBody() { //arrange var expectedContent = "{\"exceptionType\":\"ArgumentNullException\",\"exceptionMessage\":\"Value cannot be null.\"}"; RequestDelegate mockNextMiddleware = (HttpContext) => { return Task.FromException(new ArgumentNullException()); }; var httpContext = new DefaultHttpContext(); httpContext.Response.Body = new MemoryStream(); var exceptionHandlingMiddleware = new ExceptionHandlingMiddleware(mockNextMiddleware); //act await exceptionHandlingMiddleware.InvokeAsync(httpContext); httpContext.Response.Body.Position = 0; var bodyContent = ""; using (var sr = new StreamReader(httpContext.Response.Body)) bodyContent = sr.ReadToEnd(); Assert.AreEqual(expectedContent, bodyContent); }
Code language: C# (cs)

Note that you have to set Body.Position = 0 before reading the stream, otherwise the stream reader won’t return anything.

Deserializing the response JSON content

The example above verified JSON content by doing a string comparison. If you want to deserialize JSON content for a deeper test, you can use the following call:

httpContext.Response.Body.Position = 0; var exceptionResponse = await JsonSerializer.DeserializeAsync<ExceptionResponse>(httpContext.Response.Body, new JsonSerializerOptions(JsonSerializerDefaults.Web));
Code language: C# (cs)

Note: This is using System.Text.Json.

If you don’t set Body.Position = 0, then you’ll get the following exception:

System.Text.Json.JsonException: The input does not contain any JSON tokens. Expected the input to start with a valid JSON token, when isFinalBlock is true.

Modify the request

If your middleware function does something with the request, then you’ll want to be able to change the request in each unit test. By using DefaultHttpContext, you can set the request to whatever you need.

Here’s an example of adding a request header:

//arrange var expectedException = new ArgumentNullException(); RequestDelegate mockNextMiddleware = (HttpContext) => { return Task.FromException(expectedException); }; var httpContext = new DefaultHttpContext(); httpContext.Request.Headers.Add("Debug", "1");
Code language: C# (cs)

For reference – the code under test

In case you’re wondering, here is the exception handling middleware class that’s being unit tested:

public class ExceptionHandlingMiddleware { private readonly RequestDelegate NextMiddleware; private readonly JsonSerializerOptions jsonOptions; public ExceptionHandlingMiddleware(RequestDelegate nextMiddleware) { NextMiddleware = nextMiddleware; jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); } public async Task InvokeAsync(HttpContext context) { try { await NextMiddleware(context); } catch (Exception ex) { context.Response.ContentType = "application/json"; context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; await context.Response.WriteAsync(JsonSerializer.Serialize(new ExceptionResponse { ExceptionType=ex.GetType().Name, ExceptionMessage = ex.Message }, jsonOptions)); } } } public class ExceptionResponse { public string ExceptionType { get; set; } public string ExceptionMessage { get; set; } }
Code language: C# (cs)

Leave a Comment