Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Testing response.WriteAsync() in custom middleware

I have an ASP.NET Core API that I have written custom middleware for so that I can handle exceptions and write logs in a single spot. The middleware works as required when debugging via Kestrel and submitting a request from a browser or postman however in my test the response body is always a null stream.

Below is the middleware class and the test that I have written, the context.Response.WriteAsync(result) doesn't seem to flush the stream for some reason but I don't know why. Is anyone able to explain?

using System;
using System.Net;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using Microsoft.Extensions.Logging;
using System.IO;

namespace APP.API.Middleware
{
    public class ExceptionHandler
    {
        private readonly RequestDelegate request;
        private readonly ILogger logger;

        public ExceptionHandler(RequestDelegate request, ILogger<ExceptionHandler> logger)
        {
            this.request = request;
            this.logger = logger;
        }

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await request(context);
            }
            catch (Exception ex)
            {
                await HandleExceptionAsync(context, ex);
            }
        }

        private Task HandleExceptionAsync(HttpContext context, Exception ex)
        {
            HttpStatusCode statusCode = HttpStatusCode.InternalServerError;
            logger.LogError(ex, "Fatal exception");

            var result = JsonConvert.SerializeObject(new { error = ex.Message });
            context.Response.ContentType = "application/json";
            context.Response.StatusCode = (int)statusCode;

            return context.Response.WriteAsync(result);
        }
    }
}

using System.IO;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace APP.Testing.Middleware
{
    [TestClass]
    public class ExceptionHandler
    {
        [TestMethod]
        public void HandleException()
        {
            var exceptionHandler = new API.Middleware.ExceptionHandler((innerHttpContext) =>
            {
                throw new System.Exception("Test exception");
            }, new NullLogger<API.Middleware.ExceptionHandler>());

            var context = new DefaultHttpContext();

            exceptionHandler.Invoke(context).Wait();

            context.Response.Body.Seek(0, SeekOrigin.Begin);
            var reader = new StreamReader(context.Response.Body);
            var text = reader.ReadToEnd();

        }
    }
}

like image 970
ChrisMB Avatar asked Jun 30 '19 10:06

ChrisMB


People also ask

How does middleware work in NET Core?

Middleware is software that's assembled into an app pipeline to handle requests and responses. Each component: Chooses whether to pass the request to the next component in the pipeline. Can perform work before and after the next component in the pipeline.

Where do I register middleware in .NET Core?

Search for word "middleware" in the top right search box as shown below. Select Middleware Class item and give it a name and click on Add button. This will add a new class for the middleware with extension method as shown below.


1 Answers

Welcome to Stack Overflow!

Your response body is empty, because you are writing to a NullStream (not to be confused with null value).

"A Stream with no backing store. Use Null to redirect output to a stream that will not consume any operating system resources. When the methods of Stream that provide writing are invoked on Null, the call simply returns, and no data is written. Null also implements a Read method that returns zero without reading data." - Docs

Default value of Body property of HttpResponse is precisely the NullStream. In a real scenario when an HTTP request arrives, the NullStream is replaced with HttpResponseStream. You won't be able to use it on your own as its accessibility level is set to internal.

Solution

As unit testing is only simulating real scenario, you can just replace the NullStream with any type of stream you want, for example the MemoryStream:

var exceptionHandler = new ExceptionHandler((innerHttpContext) =>
{
    throw new Exception("Test exception");
}, new NullLogger<ExceptionHandler>());

var context = new DefaultHttpContext();
context.Response.Body = new MemoryStream(); // <== Replace the NullStream

await exceptionHandler.Invoke(context);

context.Response.Body.Seek(0, SeekOrigin.Begin);
var reader = new StreamReader(context.Response.Body);
var text = reader.ReadToEnd();

Don't forget to add some asserts at the end of your unit tests. After all, you want to perform some checks, right?

Assert.IsFalse(string.IsNullOrEmpty(text));

EDIT #1

As @nkosi pointed out, unless you have a really good reason, you should always call asynchronous methods with await keyword:

await exceptionHandler.Invoke(context);

and mark the method definition with async and make it return a Task:

public async Task HandleException()

That way you are avoiding deadlocks.

Something also worth pointing out (but not a necessity) is a naming convention for testing classes. Obviously, you can name it how you like, but keep it mind that when your testing class have the same name as the class you want to test, you end up with unnecessary name ambiguity. Of course you can write full name with namespace (as you did), but with my lazy nature, that's just too much so I'm using different name for testing class, for example ExceptionHandlerTests.

like image 59
Prolog Avatar answered Oct 15 '22 00:10

Prolog