How to unit test a .NET middleware that uses Response.OnStarting
Asked Answered
C

4

10

So, I created a .net core middleware that adds a header to a response. I am not finding a way to unit test this middleware, as it uses the OnStarting callback and I can't imagine how to mock or force its execution.

Here is a sample of the middleware:

public async Task InvokeAsync(HttpContext context, RequestDelegate next)
{
    context.Response.OnStarting(() =>
    {
        context.Response.Headers.Add("Name", "Value");

        return Task.CompletedTask;
    });

    await next(context);
}

As the middleware interface expects a concrete instance of HttpContext, I can't imagine a way to mock it using FakeItEasy, for example.

Coalfield answered 3/1, 2020 at 18:15 Comment(3)
Does this answer your question? Mock HttpContext for unit testing a .NET core MVC controller?Organometallic
@Organometallic Hmm, no.. I am expecting to be able to unit test this middleware without involving a controller (or the whole pipeline).Coalfield
I linked to that QA because it shows how to construct a HttpContext instance for testing - not because it instantiated a Controller.Organometallic
O
11

You can use the example in Mock HttpContext for unit testing a .NET core MVC controller? as a guide:

using Shouldly;
using Xunit;

[Fact]
public async Task Middleware_should_add_header()
{
    // Arrange:

    HttpContext ctx = new DefaultHttpContext();
    
    RequestDelegate next = ( HttpContext hc ) => Task.CompletedTask;

    MyMiddleware mw = new MyMiddleware();
    
    // Act - Part 1: InvokeAsync set-up:

    await mw.InvokeAsync( ctx, next );

    // Assert - Part 1

    ctx.Response.Headers.TryGetValue( "Name", out String value0 ).ShouldBeFalse();
    value0.ShouldBeNull();

    // Act - Part 2

    await ctx.Response.StartAsync( default );

    // Asset - Part 2

    ctx.Response.Headers.TryGetValue( "Name", out String value1 ).ShouldBeTrue();
    value1.ShouldBe( "Value" );
    
}
Organometallic answered 3/1, 2020 at 18:22 Comment(7)
Oh, I have tried a similar approach but without the "Headers.TryGetValue" and "Response.StartAsync". The error I was receiving was exactly that the response has not started.Coalfield
I am going to test here. Thank you!!Coalfield
@Organometallic I have tried this but await ctx.Response.StartAsync( default ); doesn't actually start the Response for me. I still get HasStarted as false and it doesn't go to OnStarting function. Any insight?Macro
@KemalTezerDilsiz Are you using xUnit? Do other async methods work as-expected? What version of .NET Core and ASP.NET Core are you using?Organometallic
@Organometallic I'm using Moq and latest ASP.NET Core, like this (a bit simplified): gist.github.com/kedilsiz/d04f6d00a0414c11cb6957ed4edddf85Macro
@KemalTezerDilsiz You're calling OnStarting twice in that code. Are you sure that's correct? Your InvokeAsync method should be called without the need for any OnStartingCallback methods.Organometallic
I tried both with and without those. Didn't make s difference :(Macro
S
6

What about doing it as Microsoft suggests? https://learn.microsoft.com/en-us/aspnet/core/test/middleware?view=aspnetcore-3.1

You can create your host only with the services (mocked or stubbed as you want) your middleware needs, without controllers. Then attach a TestServer to it and use it to request your api.

Maybe this documentation was not available at the time of the question, but now seems to be the best way to achieve what is wanted.

Note: Not sure if those are still considered unit tests, it depends on its definition for each developer.

Sidwell answered 8/1, 2021 at 14:26 Comment(4)
How did you get the Configure part to work? For me, the exact same code that MS provides does not build, it lacks that method entirely. It is probably a namespace issue although I cannot tell which one is the culprit as I only use Microsoft.Extensions.Hosting, Microsoft.AspNetCore.TestHost and XUnit, besides System.Threading.Tasks, of course.Mumford
@rTECH Maybe Microsoft.AspNetCore.Hosting?Sidwell
That is the namespace for HostBuilder, unless there is another implementation inside TestHost that my VS 2019 cannot find for some reason.Mumford
Essentially, you were right. The Hostbuilder is in Extensions.Hosting, but I needed to add AspNetCore.Hosting as well although there is no Hostbuilder in AspNetCore.Hosting, only a WebhostBuilder, but that does not appear to be the same.Mumford
I
4

I came across a similar requirement today and I did not find any straightforward answer. I even tried the solution posted by Dai above, which was not working for me. 

While going through the code, I noticed that OnStarting is a virtual method part of the abstract HttpResponse class that can be mocked. With that concept, I was able to find a working solution which I am posting here:

Sample Middleware:

public class ApiContextMiddleware
{
    private readonly RequestDelegate _next;
    public ApiContextMiddleware(RequestDelegate next)
    {
        _next = next;
    }
    public async Task InvokeAsync(HttpContext context)
    {
        context.Response.OnStarting(() =>
        {
            context.Response.Headers.Add("NetVersion", "VersionSix");

            return Task.CompletedTask;
        });

        await _next(context);
    }
}

For this solution, we need to mock HttpContext and HttpResponse as some properties of HttpContext are read-only (get only).

Mock<HttpContext> httpContextMock = new();
Mock<HttpResponse> httpResponseMock = new();

Now create a Func<Task> variable to hold your callback method and mock the OnStarting method of the httpResponseMock mock object like below.

Func<Task>? callbackMethod = null;

httpResponseMock.Setup(x => 
            x.OnStarting(It.IsAny<Func<Task>>()))
                .Callback<Func<Task>>(m => callbackMethod = m);

Next, mock HttpResponse and HttpResponse.Headers of the httpContextMock mock object to return httpResponseMock.Object and a HeaderDictionary object, respectively.

httpContextMock.SetupGet(x => x.Response)
            .Returns(httpResponseMock.Object);

        httpContextMock.SetupGet(x => x.Response.Headers)
            .Returns(new HeaderDictionary());

Now, create a RequestDelegate method, which is required for the middleware class, where we will be invoking the OnStarting Method.

var requestDelegate = new RequestDelegate(async (innerContext) =>
{
    isNextDelegateCalled = true;
    if (callbackMethod != null)
    {
        await callbackMethod.Invoke();
    }
    else
    {
        await Task.CompletedTask;
    }
});

That's it. Now you will be able to assert the response header key and value, added by your OnStarting callback method.

Here is the full test method:

    [Fact]
    public async Task ApiContextMiddleware_Should_Add_Header_Name_To_Http_Response()
    {
        bool isNextDelegateCalled = false;
        Mock<HttpContext> httpContextMock = new();
        Mock<HttpResponse> httpResponseMock = new();
        Func<Task>? callbackMethod = null;

        httpResponseMock.Setup(x =>
            x.OnStarting(It.IsAny<Func<Task>>()))
                .Callback<Func<Task>>(m => callbackMethod = m);

        httpContextMock.SetupGet(x => x.Response)
            .Returns(httpResponseMock.Object);

        httpContextMock.SetupGet(x => x.Response.Headers)
            .Returns(new HeaderDictionary());

        var fakeHttpContext = httpContextMock.Object;

        var requestDelegate = new RequestDelegate(async (innerContext) =>
        {
            isNextDelegateCalled = true;

            if (callbackMethod != null)
            {
                await callbackMethod.Invoke();
            }
            else
            {
                await Task.CompletedTask;
            }
        });

        var middelware = new ApiContextMiddleware(requestDelegate);

        await middelware.InvokeAsync(fakeHttpContext);

        Assert.True(isNextDelegateCalled);
        Assert.True(fakeHttpContext.Response.Headers.TryGetValue("NetVersion", out var value));
        Assert.Equal("VersionSix", value);
    }
Incestuous answered 17/6, 2022 at 21:25 Comment(1)
This doesn't seem to work when we need to insert objects into the http context.Larsen
C
0

It's an older question and I was looking for a new way to test my MiddleWare, however you could also test API endpoints like this in a "integration test"

I am using NuGet package Microsoft.AspNetCore.TestHost to make a "Fake" web server, I am assuming you can use Microsoft nuget packages.

then I generate MiddleWare that I can use to "plug" the HttpContext class with my test case using an implementation of ConectionInfo.

public class MyConnection : ConnectionInfo
{
    ConnectionInfo? _other = null;
    public override string Id { get; set; } = Guid.NewGuid().ToString();
    public override IPAddress? RemoteIpAddress { get; set; }
    public override int RemotePort { get; set; }
    public override IPAddress? LocalIpAddress { get; set; }
    public override int LocalPort { get; set; }
    public override X509Certificate2? ClientCertificate { get; set; }

    public override Task<X509Certificate2?> GetClientCertificateAsync(CancellationToken cancellationToken = default)
    {
        return _other?.GetClientCertificateAsync(cancellationToken) ?? Task.FromResult<X509Certificate2?>(null);
    }
    public void Populate(ConnectionInfo other)
    {
        other.ClientCertificate = this.ClientCertificate;
        other.RemoteIpAddress = this.RemoteIpAddress;
        other.LocalIpAddress = this.LocalIpAddress;
        other.RemotePort = this.RemotePort;
        other.LocalPort = this.LocalPort;
    }
}

I create a middleware class that I inject as first so that I am called first as the purpose is to update my HttpContext class with my test case; here is the code:

public class PrepareForTest { private readonly RequestDelegate _next; private readonly MyConnection _connection;

//inject anything you need to hack
public PrepareForTest(RequestDelegate next, MyConnection connection)
{
    _next = next;
    _connection = connection;
}
public async Task Invoke(HttpContext context)
{
    //populate the http.connection object
    _connection.Populate(context.Connection);
    //let the web server do it's thing
    await _next(context);            
}

}

Now we can write the test, in my case, I am testing middleware injected via dependency injection extension named AddSecurityMiddleWare

Here is the code, it will allow "8.8.8.8" to access the "/" route

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.TestHost;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using System.Net;
using System.Net.Http.Headers;
using System.Security.Cryptography.X509Certificates;
using System.Text;
[TestClass()]
public class SecurityMiddlewareTests
{

    [TestMethod()]
    public async Task InvokeTest()
    {

        var connection = new MyConnection
        {
            LocalIpAddress = IPAddress.Loopback,
            LocalPort = 80,
            RemotePort = 80,
            RemoteIpAddress = IPAddress.Parse("8.8.8.8"),
        };

        var configuration = new ConfigurationBuilder()
                                .AddJsonFile("AppSettings.Development.json")
                                .Build();

        using var host = await new HostBuilder()
                                    .ConfigureWebHost(webBuilder =>
                                    {
                                        webBuilder.UseTestServer(configureOptions =>
                                        {
                                            configureOptions.BaseAddress = new Uri("http://www.vesnx.com");
                                        })
                                        .ConfigureServices(services =>
                                        {
                                            services.AddSingleton<IConfiguration>(configuration);
                                            services.AddSingleton<MyConnection>(connection);
                                            //Load Controllers from a class that I am testing if I am testing controllers
                                            //services.AddMvc().AddApplicationPart(typeof(IPAddressController).Assembly);
                                            //use the same dependency injection as production
                                            services.AddLogging()
                                                    .AddIdpsDmzTools();
                                            //needed when you like to call MapGet
                                            services.AddRouting();

                                        })
                                        .Configure(app =>
                                        {
                                            //first execute the text fixture
                                            app.UseMiddleware<PrepareForTest>();

                                            //add the production code as it would be in production via dependency injection
                                            app.AddSecurityMiddleWare();

                                            //if you are interested in the answer make sure you get one
                                            app.UseRouting();
                                            app.UseEndpoints(endpoints =>
                                            {
                                                endpoints.MapGet("/", () => "Hello World here is some value");

                                            });
                                        });
                                    }).StartAsync();

        //generate a client for the test
        using var client = host.GetTestClient();

        //set the browser headers as needed
        client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(Encoding.UTF8.GetBytes("login:password")));
        client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("Edge", "0.1.1"));
        client.DefaultRequestHeaders.AcceptLanguage.Add(new StringWithQualityHeaderValue("en-US", 1.0));
        client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip", 1.0));
        client.DefaultRequestHeaders.AcceptCharset.Add(new StringWithQualityHeaderValue("utf-8", 1.0));

        //execute the request
        using var req = await client.GetAsync("/");

        //validate the expected response for the client
        Assert.IsTrue(req.IsSuccessStatusCode);
        var text = await req.Content.ReadAsStringAsync();
        Assert.IsTrue(text.Contains("some value"));

    }
}

You can test API endpoint, RazorPages... just update the UseTestServer() to match your test case.

Crenshaw answered 21/2, 2023 at 10:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.